Compare commits
436 Commits
v0.107.20
...
2499-rewri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1be2bab4d | ||
|
|
53cd9b7a1a | ||
|
|
d8d7a5c335 | ||
|
|
18a6066df5 | ||
|
|
18392943fa | ||
|
|
c2abedec70 | ||
|
|
bbdcc673a2 | ||
|
|
d3bf5fcb05 | ||
|
|
5a794411d9 | ||
|
|
8e058b8042 | ||
|
|
d76834f843 | ||
|
|
e7fc61a997 | ||
|
|
97af23b0af | ||
|
|
5480bed1f7 | ||
|
|
c5fb7e6b0d | ||
|
|
9efc381224 | ||
|
|
e481922d91 | ||
|
|
defde7d0fe | ||
|
|
0c03063c8a | ||
|
|
0ddd8e3dcc | ||
|
|
48cbc7bdf0 | ||
|
|
299371e0fd | ||
|
|
12f52f07c5 | ||
|
|
de08ef0077 | ||
|
|
cfab157146 | ||
|
|
ec05ee16fe | ||
|
|
c1b537c14b | ||
|
|
990311c9e0 | ||
|
|
526c358697 | ||
|
|
d77b743c7b | ||
|
|
e657899c32 | ||
|
|
fb3602853a | ||
|
|
2cf171f21e | ||
|
|
e56f465ad8 | ||
|
|
a8e80bc583 | ||
|
|
9a186d0a8a | ||
|
|
2d29455d7f | ||
|
|
8d453e75a4 | ||
|
|
de9f9e9eb8 | ||
|
|
fa49d74aa8 | ||
|
|
f0cf6cce9a | ||
|
|
55a0dec144 | ||
|
|
6d1adf74b1 | ||
|
|
6b607e982b | ||
|
|
01652e6ab2 | ||
|
|
09f88cf21d | ||
|
|
e6f8aeeebe | ||
|
|
fafd7a1e82 | ||
|
|
53a366ed46 | ||
|
|
9c4bed31e7 | ||
|
|
23c16a13aa | ||
|
|
36d90b152e | ||
|
|
08282dc4d9 | ||
|
|
93882d6860 | ||
|
|
167b112511 | ||
|
|
98af0e000e | ||
|
|
2bfdcbbc10 | ||
|
|
8fdbcc005c | ||
|
|
464fbf0b54 | ||
|
|
a7d02fa935 | ||
|
|
af8f64ac00 | ||
|
|
c139287787 | ||
|
|
fa0fd90ddd | ||
|
|
c5565a9e4e | ||
|
|
ac7634da37 | ||
|
|
746e9df727 | ||
|
|
3dd7393b3f | ||
|
|
9c9d6b48e3 | ||
|
|
9951d861d1 | ||
|
|
8a935d4ffb | ||
|
|
bf10f157ab | ||
|
|
15f5876e33 | ||
|
|
04c8e3b288 | ||
|
|
cebbb69a4c | ||
|
|
a272b61ed6 | ||
|
|
b86250737e | ||
|
|
a149d816d9 | ||
|
|
67d89660ca | ||
|
|
2a85d7dd7e | ||
|
|
68d13fcc2b | ||
|
|
2de42284a5 | ||
|
|
d2a09e49ff | ||
|
|
e0080ffa3a | ||
|
|
8dba4ecd01 | ||
|
|
aaaa56fce3 | ||
|
|
ab79168b13 | ||
|
|
5ae826d8a9 | ||
|
|
a736f67205 | ||
|
|
fee81b31ec | ||
|
|
a1acfbbae4 | ||
|
|
4582b1c919 | ||
|
|
893358ea71 | ||
|
|
f109fb17a4 | ||
|
|
5604e33574 | ||
|
|
67da002391 | ||
|
|
d42d1a7ea4 | ||
|
|
e4a42bf233 | ||
|
|
0eba31ca03 | ||
|
|
f5602d9c46 | ||
|
|
f1dd33346a | ||
|
|
960a7a75ed | ||
|
|
a126f514ff | ||
|
|
c0c9d8adb0 | ||
|
|
7cac010573 | ||
|
|
51f426736c | ||
|
|
0c0340d63e | ||
|
|
330ac30324 | ||
|
|
2e0f6e5468 | ||
|
|
b7e815483e | ||
|
|
15b19ff726 | ||
|
|
f557339ca0 | ||
|
|
fe8be3701f | ||
|
|
c26ab190e7 | ||
|
|
6a62f704e2 | ||
|
|
24eb3476db | ||
|
|
8a924cb4ed | ||
|
|
6e7964c9e7 | ||
|
|
9d59be4269 | ||
|
|
bf792b83f6 | ||
|
|
0cce420261 | ||
|
|
61bd217eb3 | ||
|
|
739e0098ec | ||
|
|
27032ef79e | ||
|
|
5e626306d1 | ||
|
|
2ffea605cf | ||
|
|
4d404b887f | ||
|
|
7b48863041 | ||
|
|
756b14a61d | ||
|
|
b71a5d86de | ||
|
|
d45fa5801e | ||
|
|
47c9c946a3 | ||
|
|
690deb1c05 | ||
|
|
59d18c6598 | ||
|
|
91bbb744dc | ||
|
|
11e4f09165 | ||
|
|
c45c02de29 | ||
|
|
fe0c53ec43 | ||
|
|
4fc045de11 | ||
|
|
cc2388e0c8 | ||
|
|
ab6da05b51 | ||
|
|
8e89cc129c | ||
|
|
9ffe078703 | ||
|
|
27b0251b5b | ||
|
|
ed209daf8a | ||
|
|
95771c7aba | ||
|
|
42bd0615c2 | ||
|
|
3a88ef3be2 | ||
|
|
572fed9f35 | ||
|
|
663f0643f2 | ||
|
|
fc62796e2d | ||
|
|
b9e39c8cca | ||
|
|
fffa656758 | ||
|
|
b74b92fc27 | ||
|
|
bc1503af57 | ||
|
|
b79c08316f | ||
|
|
08799e9d0a | ||
|
|
bedfb47a9f | ||
|
|
53e2c1f7cd | ||
|
|
88812f05f5 | ||
|
|
10a8f79644 | ||
|
|
ccc4f1a2da | ||
|
|
451fd7c445 | ||
|
|
782de99a0a | ||
|
|
d4afd60b08 | ||
|
|
c8ace868d4 | ||
|
|
2b4158e5c9 | ||
|
|
53209bc42c | ||
|
|
da1ae33805 | ||
|
|
ab02c829ea | ||
|
|
3c0d2a9253 | ||
|
|
58512c3af9 | ||
|
|
78389e518e | ||
|
|
9c9169ac12 | ||
|
|
e545f3bdb7 | ||
|
|
c000d9f232 | ||
|
|
1fb043768e | ||
|
|
3660b4810e | ||
|
|
a9127c4a45 | ||
|
|
c098960b39 | ||
|
|
5cc2a2cd0c | ||
|
|
8733f55c2c | ||
|
|
a3750ffff1 | ||
|
|
9e0d3eb6e7 | ||
|
|
e0a57d2912 | ||
|
|
53e77cb2c0 | ||
|
|
8ecfef16eb | ||
|
|
d51110acb5 | ||
|
|
2348b8fafa | ||
|
|
7f0b16d074 | ||
|
|
a0c8aee3f7 | ||
|
|
d519929988 | ||
|
|
cb83f8b531 | ||
|
|
45bcc2c09a | ||
|
|
2410639123 | ||
|
|
d1525cf09d | ||
|
|
35c1d84b42 | ||
|
|
986124948a | ||
|
|
fa76ad2a3c | ||
|
|
57c0b1203e | ||
|
|
be1bc76cfa | ||
|
|
6913ebb29f | ||
|
|
e35eeacd74 | ||
|
|
bdcf345155 | ||
|
|
307654f648 | ||
|
|
970b6cf698 | ||
|
|
eccfbf6a6d | ||
|
|
1a1a48482a | ||
|
|
1afd73ad0b | ||
|
|
6856a80380 | ||
|
|
cf3a8991ea | ||
|
|
e3624ec588 | ||
|
|
64df882c5e | ||
|
|
06e4658da9 | ||
|
|
4a7b4d03a1 | ||
|
|
257d167002 | ||
|
|
e6ebb8efef | ||
|
|
7e80980ae4 | ||
|
|
50476cda31 | ||
|
|
ea5d165a70 | ||
|
|
2830f396c6 | ||
|
|
620ad13490 | ||
|
|
f54a2dc1da | ||
|
|
63f6844318 | ||
|
|
12edc05ab0 | ||
|
|
71b8e75138 | ||
|
|
0bcc6699e1 | ||
|
|
385a873b0f | ||
|
|
0daa6a107b | ||
|
|
72098d2255 | ||
|
|
572d2794e2 | ||
|
|
d4c3a43bcb | ||
|
|
6e63757fc7 | ||
|
|
721397cee3 | ||
|
|
fd1c841810 | ||
|
|
f58265ec98 | ||
|
|
14fd995ae9 | ||
|
|
50565bed3b | ||
|
|
70f85fca21 | ||
|
|
4293cf5945 | ||
|
|
4c6377c5cb | ||
|
|
9b3adac145 | ||
|
|
73f935f3f3 | ||
|
|
a481ff4c51 | ||
|
|
bbccd61614 | ||
|
|
8a3d5f046c | ||
|
|
eb8e8166c8 | ||
|
|
3420becce3 | ||
|
|
9ed8699c75 | ||
|
|
b59b82474a | ||
|
|
cce0e593c5 | ||
|
|
da32079516 | ||
|
|
ccf268baf4 | ||
|
|
053bb72a00 | ||
|
|
41f081d8da | ||
|
|
e0f2c3d170 | ||
|
|
f32da12a86 | ||
|
|
f5959a0dc6 | ||
|
|
0a5888f27a | ||
|
|
07d48af10c | ||
|
|
e58a415d10 | ||
|
|
ae43ca0605 | ||
|
|
9acb1f364b | ||
|
|
84cd528103 | ||
|
|
56519548f1 | ||
|
|
bdcd17a41a | ||
|
|
1eafb4e7cf | ||
|
|
bf024fb985 | ||
|
|
a832987f7c | ||
|
|
77e5e27d75 | ||
|
|
3505ce8739 | ||
|
|
14d8f58592 | ||
|
|
006cd98869 | ||
|
|
ce1b2bc4f1 | ||
|
|
8f4acce44a | ||
|
|
b04d1ed6c8 | ||
|
|
f987c25598 | ||
|
|
b9b93f1286 | ||
|
|
a7a5e50620 | ||
|
|
0edf71a4af | ||
|
|
5956b97e7f | ||
|
|
d3f39b0aa1 | ||
|
|
e738508d7a | ||
|
|
302faca32f | ||
|
|
1c1ca1c6e3 | ||
|
|
a497dc09ca | ||
|
|
3ce04f48ca | ||
|
|
368a98fb29 | ||
|
|
cbe32c5a73 | ||
|
|
f46c9f74d5 | ||
|
|
4b884ace62 | ||
|
|
7ce7e90865 | ||
|
|
756c932e37 | ||
|
|
c3d5fcc669 | ||
|
|
65a33a1215 | ||
|
|
1a49d2f0c9 | ||
|
|
549b20bdea | ||
|
|
75f01d51f7 | ||
|
|
a82ec09afd | ||
|
|
c0ac82be6a | ||
|
|
24d7dc8e8a | ||
|
|
79d85a24e9 | ||
|
|
f289f4b1b6 | ||
|
|
b7eedb3feb | ||
|
|
58515fce43 | ||
|
|
21905d9869 | ||
|
|
56f78edb97 | ||
|
|
a580149ad6 | ||
|
|
6dc9e73ce4 | ||
|
|
5d52e68d26 | ||
|
|
c4ff80fd3a | ||
|
|
ed449c6186 | ||
|
|
1c89394aef | ||
|
|
235316e050 | ||
|
|
0a1ff65b4a | ||
|
|
2a1ad532f4 | ||
|
|
9d144ecb0a | ||
|
|
9b7fe74086 | ||
|
|
0f2a9f262e | ||
|
|
82af43039c | ||
|
|
12ee287d0b | ||
|
|
57171f0a61 | ||
|
|
21a1187ed2 | ||
|
|
2c2c0d445b | ||
|
|
9f0fdc5e78 | ||
|
|
96594a3433 | ||
|
|
4c5b38a447 | ||
|
|
0e608fda13 | ||
|
|
8bb95469d9 | ||
|
|
e9e0b7c4f9 | ||
|
|
c70f941bf8 | ||
|
|
a79b61aac3 | ||
|
|
5e71f5df6a | ||
|
|
047970e5ee | ||
|
|
f31ffcc5d1 | ||
|
|
0d562a7b1f | ||
|
|
3603b1fcab | ||
|
|
82505566f8 | ||
|
|
9ce2a0fb34 | ||
|
|
5cba78a8d5 | ||
|
|
2c33ab6a92 | ||
|
|
beb674ecbc | ||
|
|
b16b1d1d24 | ||
|
|
f8e45c13f3 | ||
|
|
b9790f663a | ||
|
|
778585865e | ||
|
|
cd8206ad9b | ||
|
|
573cbafe3f | ||
|
|
c346216424 | ||
|
|
e7b3c9969b | ||
|
|
dc0d081b47 | ||
|
|
ded9842cd7 | ||
|
|
89d9b03dfe | ||
|
|
f1d05a49f0 | ||
|
|
9a764b9b82 | ||
|
|
e0b557eda2 | ||
|
|
ea6e033dae | ||
|
|
3afe7c3daf | ||
|
|
afbc7a72e3 | ||
|
|
ff1e108bfe | ||
|
|
b29f320fd4 | ||
|
|
773b80a969 | ||
|
|
975995a9c7 | ||
|
|
f131067278 | ||
|
|
b43aa86cae | ||
|
|
6824eec308 | ||
|
|
18079ca1bb | ||
|
|
a1f29c31b9 | ||
|
|
0ef8344178 | ||
|
|
f53f48cc33 | ||
|
|
2a5b5f1927 | ||
|
|
b290eddc70 | ||
|
|
6d0a43aad6 | ||
|
|
1bc2186c2d | ||
|
|
6584c300b8 | ||
|
|
dc480ae70f | ||
|
|
e783564084 | ||
|
|
0ee34534c6 | ||
|
|
9146df5493 | ||
|
|
76fa60498e | ||
|
|
8455940b59 | ||
|
|
2d46aa7121 | ||
|
|
bf9b35b9c6 | ||
|
|
f9aa5ae86a | ||
|
|
642d68c482 | ||
|
|
5ff7cdbac8 | ||
|
|
504c54ab0e | ||
|
|
90c17c79de | ||
|
|
0b72bcc5a1 | ||
|
|
dc14f89c9f | ||
|
|
2263adbbe0 | ||
|
|
e29261516f | ||
|
|
f12eaf29a2 | ||
|
|
3e2ab87293 | ||
|
|
41e8db4221 | ||
|
|
3f5605c42e | ||
|
|
f7ff02f3b1 | ||
|
|
5ec4a4dab8 | ||
|
|
13871977f9 | ||
|
|
2fdda8a22c | ||
|
|
d82b290251 | ||
|
|
eb15304ff4 | ||
|
|
1a3bf5ebda | ||
|
|
15956f4511 | ||
|
|
09d0ce4578 | ||
|
|
9735a35123 | ||
|
|
813a06d09a | ||
|
|
061136508e | ||
|
|
008f58c863 | ||
|
|
0e4ffd339f | ||
|
|
1458600c37 | ||
|
|
34c95f99f8 | ||
|
|
e9c59b098e | ||
|
|
a0bb5ce8a4 | ||
|
|
01947bedb4 | ||
|
|
a6ca824064 | ||
|
|
380cff07f2 | ||
|
|
d2ce06e1ca | ||
|
|
2ed1f939b5 | ||
|
|
dea8a585f8 | ||
|
|
313555b10c | ||
|
|
661f4ece48 | ||
|
|
52f36f201e | ||
|
|
46cd974e2a | ||
|
|
201ef10de6 | ||
|
|
d9df7c13be | ||
|
|
64e751e579 | ||
|
|
e6e5958595 | ||
|
|
ff3df0ec33 | ||
|
|
ebe86ce00e | ||
|
|
d317e19291 | ||
|
|
39c4999d2d | ||
|
|
7f55bd8461 | ||
|
|
2968a65f14 | ||
|
|
779fbe79b8 | ||
|
|
da0d1cb754 | ||
|
|
ef80c07075 |
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -7,9 +7,9 @@
|
|||||||
'name': 'AdGuard filters issues'
|
'name': 'AdGuard filters issues'
|
||||||
'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github'
|
'url': 'https://link.adtidy.org/forward.html?action=report&app=home&from=github'
|
||||||
- 'about': >
|
- 'about': >
|
||||||
Please send requests for addition to the vetted filtering lists to the
|
Please send requests for new blocked services and vetted filtering lists
|
||||||
Hostlists Registry repository.
|
to the Hostlists Registry repository
|
||||||
'name': 'AdGuard Hostlists Registry'
|
'name': 'Blocked services and vetted filtering rule lists: AdGuard Hostlists Registry'
|
||||||
'url': 'https://github.com/AdguardTeam/HostlistsRegistry'
|
'url': 'https://github.com/AdguardTeam/HostlistsRegistry'
|
||||||
- 'about': >
|
- 'about': >
|
||||||
Please use GitHub Discussions for questions
|
Please use GitHub Discussions for questions
|
||||||
|
|||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.8'
|
'GO_VERSION': '1.18.9'
|
||||||
'NODE_VERSION': '14'
|
'NODE_VERSION': '14'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
|||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.8'
|
'GO_VERSION': '1.18.9'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|||||||
47
CHANGELOG.md
47
CHANGELOG.md
@@ -18,12 +18,48 @@ and this project adheres to
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## [v0.107.21] - 2122-12-28 (APPROX.)
|
## [v0.107.22] - 2222-12-28 (APPROX.)
|
||||||
|
|
||||||
|
See also the [v0.107.22 GitHub milestone][ms-v0.107.22].
|
||||||
|
|
||||||
|
[ms-v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/milestone/58?closed=1
|
||||||
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## [v0.107.21] - 2122-12-15
|
||||||
|
|
||||||
See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
|
See also the [v0.107.21 GitHub milestone][ms-v0.107.21].
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The URLs of the default filters for new installations are synchronized to
|
||||||
|
those introduced in v0.107.20 ([#5238]).
|
||||||
|
|
||||||
|
**NOTE:** Some users may need to re-add the lists from the vetted filter lists
|
||||||
|
to update the URLs to the new ones. Custom filters added by users themselves
|
||||||
|
do not require re-adding.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- `AdGuardHome --update` freezing when another instance of AdGuard Home is
|
||||||
|
running ([#4223], [#5191]).
|
||||||
|
- The `--update` flag performing an update even with the same version.
|
||||||
|
- Failing HTTPS redirection on saving the encryption settings ([#4898]).
|
||||||
|
- Zeroing rules counter of erroneusly edited filtering rule lists ([#5290]).
|
||||||
|
- Filters updating strategy, which could sometimes lead to use of broken or
|
||||||
|
incompletely downloaded lists ([#5258]).
|
||||||
|
- Errors popping up during updates of settings, which could sometimes cause the
|
||||||
|
server to stop responding ([#5251]).
|
||||||
|
|
||||||
|
[#4898]: https://github.com/AdguardTeam/AdGuardHome/issues/4898
|
||||||
|
[#5191]: https://github.com/AdguardTeam/AdGuardHome/issues/5191
|
||||||
|
[#5238]: https://github.com/AdguardTeam/AdGuardHome/issues/5238
|
||||||
|
[#5251]: https://github.com/AdguardTeam/AdGuardHome/issues/5251
|
||||||
|
[#5258]: https://github.com/AdguardTeam/AdGuardHome/issues/5258
|
||||||
|
[#5290]: https://github.com/AdguardTeam/AdGuardHome/issues/5290
|
||||||
|
|
||||||
[ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1
|
[ms-v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/milestone/57?closed=1
|
||||||
-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1482,11 +1518,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
|||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...HEAD
|
||||||
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
|
[v0.107.22]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...v0.107.22
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.21...HEAD
|
||||||
|
[v0.107.21]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.20...v0.107.21
|
||||||
[v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20
|
[v0.107.20]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.19...v0.107.20
|
||||||
[v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19
|
[v0.107.19]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.18...v0.107.19
|
||||||
[v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18
|
[v0.107.18]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...v0.107.18
|
||||||
|
|||||||
3
Makefile
3
Makefile
@@ -34,6 +34,8 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
|||||||
--ignore-engines --ignore-optional --ignore-platform\
|
--ignore-engines --ignore-optional --ignore-platform\
|
||||||
--ignore-scripts
|
--ignore-scripts
|
||||||
|
|
||||||
|
NEXTAPI = 0
|
||||||
|
|
||||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||||
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
|
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
|
||||||
@@ -61,6 +63,7 @@ ENV = env\
|
|||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
|
NEXTAPI='$(NEXTAPI)'\
|
||||||
VERBOSE='$(VERBOSE)'\
|
VERBOSE='$(VERBOSE)'\
|
||||||
VERSION='$(VERSION)'\
|
VERSION='$(VERSION)'\
|
||||||
|
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Налады DHCP IPv6",
|
"dhcp_ipv6_settings": "Налады DHCP IPv6",
|
||||||
"form_error_required": "Абавязковае поле",
|
"form_error_required": "Абавязковае поле",
|
||||||
"form_error_ip4_format": "Няслушны IPv4-адрас",
|
"form_error_ip4_format": "Няслушны IPv4-адрас",
|
||||||
"form_error_ip4_range_start_format": "Няслушны IPv4-адрас пачатку дыяпазону",
|
|
||||||
"form_error_ip4_range_end_format": "Няслушны IPv4-адрас канца дыяпазону",
|
|
||||||
"form_error_ip4_gateway_format": "Няслушны IPv4-адрас шлюза",
|
"form_error_ip4_gateway_format": "Няслушны IPv4-адрас шлюза",
|
||||||
"form_error_ip6_format": "Няслушны IPv6-адрас",
|
"form_error_ip6_format": "Няслушны IPv6-адрас",
|
||||||
"form_error_ip_format": "Няслушны IP-адрас",
|
"form_error_ip_format": "Няслушны IP-адрас",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»",
|
"out_of_range_error": "Павінна быць па-за дыяпазонам «{{start}}»-«{{end}}»",
|
||||||
"lower_range_start_error": "Павінна быць менш за пачатак дыяпазону",
|
"lower_range_start_error": "Павінна быць менш за пачатак дыяпазону",
|
||||||
"greater_range_start_error": "Павінна быць больш за пачатак дыяпазону",
|
"greater_range_start_error": "Павінна быць больш за пачатак дыяпазону",
|
||||||
"greater_range_end_error": "Павінна быць больш за канец дыяпазону",
|
|
||||||
"subnet_error": "Адрасы павінны быць усярэдзіне адной падсеткі",
|
"subnet_error": "Адрасы павінны быць усярэдзіне адной падсеткі",
|
||||||
"gateway_or_subnet_invalid": "Некарэктная маска падсеткі",
|
"gateway_or_subnet_invalid": "Некарэктная маска падсеткі",
|
||||||
"dhcp_form_gateway_input": "IP-адрас шлюза",
|
"dhcp_form_gateway_input": "IP-адрас шлюза",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Nastavení DHCP IPv6",
|
"dhcp_ipv6_settings": "Nastavení DHCP IPv6",
|
||||||
"form_error_required": "Povinné pole",
|
"form_error_required": "Povinné pole",
|
||||||
"form_error_ip4_format": "Neplatná adresa IPv4",
|
"form_error_ip4_format": "Neplatná adresa IPv4",
|
||||||
"form_error_ip4_range_start_format": "Neplatná adresa IPv4 na začátku rozsahu",
|
|
||||||
"form_error_ip4_range_end_format": "Neplatná adresa IPv4 na konci rozsahu",
|
|
||||||
"form_error_ip4_gateway_format": "Neplatná adresa IPv4 brány",
|
"form_error_ip4_gateway_format": "Neplatná adresa IPv4 brány",
|
||||||
"form_error_ip6_format": "Neplatná adresa IPv6",
|
"form_error_ip6_format": "Neplatná adresa IPv6",
|
||||||
"form_error_ip_format": "Neplatná IP adresa",
|
"form_error_ip_format": "Neplatná IP adresa",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Musí být mimo rozsah \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Musí být menší než začátek rozsahu",
|
"lower_range_start_error": "Musí být menší než začátek rozsahu",
|
||||||
"greater_range_start_error": "Musí být větší než začátek rozsahu",
|
"greater_range_start_error": "Musí být větší než začátek rozsahu",
|
||||||
"greater_range_end_error": "Musí být větší než konec rozsahu",
|
|
||||||
"subnet_error": "Adresy musí být v jedné podsíti",
|
"subnet_error": "Adresy musí být v jedné podsíti",
|
||||||
"gateway_or_subnet_invalid": "Neplatná maska podsítě",
|
"gateway_or_subnet_invalid": "Neplatná maska podsítě",
|
||||||
"dhcp_form_gateway_input": "IP brána",
|
"dhcp_form_gateway_input": "IP brána",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
|
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
|
||||||
"form_error_required": "Obligatorisk felt",
|
"form_error_required": "Obligatorisk felt",
|
||||||
"form_error_ip4_format": "Ugyldig IPv4-adresse",
|
"form_error_ip4_format": "Ugyldig IPv4-adresse",
|
||||||
"form_error_ip4_range_start_format": "Ugyldig IPv4-startadresse for området",
|
|
||||||
"form_error_ip4_range_end_format": "Ugyldig IPv4-slutadresse for området",
|
|
||||||
"form_error_ip4_gateway_format": "Ugyldig IPv4 gateway-adresse",
|
"form_error_ip4_gateway_format": "Ugyldig IPv4 gateway-adresse",
|
||||||
"form_error_ip6_format": "Ugyldig IPv6-adresse",
|
"form_error_ip6_format": "Ugyldig IPv6-adresse",
|
||||||
"form_error_ip_format": "Ugyldig IP-adresse",
|
"form_error_ip_format": "Ugyldig IP-adresse",
|
||||||
@@ -51,9 +49,8 @@
|
|||||||
"out_of_range_error": "Skal være uden for området \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Skal være uden for området \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Skal være mindre end starten på området",
|
"lower_range_start_error": "Skal være mindre end starten på området",
|
||||||
"greater_range_start_error": "Skal være større end starten på området",
|
"greater_range_start_error": "Skal være større end starten på området",
|
||||||
"greater_range_end_error": "Skal være større end områdeslutning",
|
|
||||||
"subnet_error": "Adresser ska være i ét undernet",
|
"subnet_error": "Adresser ska være i ét undernet",
|
||||||
"gateway_or_subnet_invalid": "Undernetmaske ugyldig",
|
"gateway_or_subnet_invalid": "Ugyldig undernetmaske",
|
||||||
"dhcp_form_gateway_input": "Gateway IP",
|
"dhcp_form_gateway_input": "Gateway IP",
|
||||||
"dhcp_form_subnet_input": "Undernetmaske",
|
"dhcp_form_subnet_input": "Undernetmaske",
|
||||||
"dhcp_form_range_title": "Interval af IP-adresser",
|
"dhcp_form_range_title": "Interval af IP-adresser",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
|
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
|
||||||
"form_error_required": "Pflichtfeld",
|
"form_error_required": "Pflichtfeld",
|
||||||
"form_error_ip4_format": "Ungültige IPv4-Adresse",
|
"form_error_ip4_format": "Ungültige IPv4-Adresse",
|
||||||
"form_error_ip4_range_start_format": "Ungültiger Bereichsbeginn der IPv4-Adresse",
|
|
||||||
"form_error_ip4_range_end_format": "Ungültiges Bereichsende der IPv4-Adresse",
|
|
||||||
"form_error_ip4_gateway_format": "Ungültige IPv4-Adresse des Gateways",
|
"form_error_ip4_gateway_format": "Ungültige IPv4-Adresse des Gateways",
|
||||||
"form_error_ip6_format": "Ungültige IPv6-Adresse",
|
"form_error_ip6_format": "Ungültige IPv6-Adresse",
|
||||||
"form_error_ip_format": "Ungültige IP-Adresse",
|
"form_error_ip_format": "Ungültige IP-Adresse",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen",
|
"out_of_range_error": "Muss außerhalb des Bereichs „{{start}}“-„{{end}}“ liegen",
|
||||||
"lower_range_start_error": "Muss niedriger als der Bereichsbeginn sein",
|
"lower_range_start_error": "Muss niedriger als der Bereichsbeginn sein",
|
||||||
"greater_range_start_error": "Muss größer als der Bereichsbeginn sein",
|
"greater_range_start_error": "Muss größer als der Bereichsbeginn sein",
|
||||||
"greater_range_end_error": "Muss größer als das Bereichsende sein",
|
|
||||||
"subnet_error": "Die Adressen müssen innerhalb eines Subnetzes liegen",
|
"subnet_error": "Die Adressen müssen innerhalb eines Subnetzes liegen",
|
||||||
"gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
|
"gateway_or_subnet_invalid": "Ungültige Subnetzmaske",
|
||||||
"dhcp_form_gateway_input": "Gateway-IP",
|
"dhcp_form_gateway_input": "Gateway-IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Configuración DHCP IPv6",
|
"dhcp_ipv6_settings": "Configuración DHCP IPv6",
|
||||||
"form_error_required": "Campo obligatorio",
|
"form_error_required": "Campo obligatorio",
|
||||||
"form_error_ip4_format": "Dirección IPv4 no válida",
|
"form_error_ip4_format": "Dirección IPv4 no válida",
|
||||||
"form_error_ip4_range_start_format": "Dirección IPv4 no válida del inicio de rango",
|
|
||||||
"form_error_ip4_range_end_format": "Dirección IPv4 no válida del final de rango",
|
|
||||||
"form_error_ip4_gateway_format": "Dirección IPv4 no válida de la puerta de enlace",
|
"form_error_ip4_gateway_format": "Dirección IPv4 no válida de la puerta de enlace",
|
||||||
"form_error_ip6_format": "Dirección IPv6 no válida",
|
"form_error_ip6_format": "Dirección IPv6 no válida",
|
||||||
"form_error_ip_format": "Dirección IP no válida",
|
"form_error_ip_format": "Dirección IP no válida",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Debe estar fuera del rango \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Debe ser inferior que el inicio de rango",
|
"lower_range_start_error": "Debe ser inferior que el inicio de rango",
|
||||||
"greater_range_start_error": "Debe ser mayor que el inicio de rango",
|
"greater_range_start_error": "Debe ser mayor que el inicio de rango",
|
||||||
"greater_range_end_error": "Debe ser mayor que el final de rango",
|
|
||||||
"subnet_error": "Las direcciones deben estar en una subred",
|
"subnet_error": "Las direcciones deben estar en una subred",
|
||||||
"gateway_or_subnet_invalid": "Máscara de subred no válida",
|
"gateway_or_subnet_invalid": "Máscara de subred no válida",
|
||||||
"dhcp_form_gateway_input": "IP de puerta de enlace",
|
"dhcp_form_gateway_input": "IP de puerta de enlace",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP:n IPv6-asetukset",
|
"dhcp_ipv6_settings": "DHCP:n IPv6-asetukset",
|
||||||
"form_error_required": "Pakollinen kenttä",
|
"form_error_required": "Pakollinen kenttä",
|
||||||
"form_error_ip4_format": "Virheellinen IPv4-osoite",
|
"form_error_ip4_format": "Virheellinen IPv4-osoite",
|
||||||
"form_error_ip4_range_start_format": "Virheellinen IPv4-osoitealueen aloitusosoite",
|
|
||||||
"form_error_ip4_range_end_format": "Virheellinen IPv4-osoitealueen päätösosoite",
|
|
||||||
"form_error_ip4_gateway_format": "Virheellinen yhdyskäytävän IPv4-osoite",
|
"form_error_ip4_gateway_format": "Virheellinen yhdyskäytävän IPv4-osoite",
|
||||||
"form_error_ip6_format": "Virheellinen IPv6-osoite",
|
"form_error_ip6_format": "Virheellinen IPv6-osoite",
|
||||||
"form_error_ip_format": "Virheellinen IP-osoite",
|
"form_error_ip_format": "Virheellinen IP-osoite",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella",
|
"out_of_range_error": "Oltava alueen \"{{start}}\" - \"{{end}}\" ulkopuolella",
|
||||||
"lower_range_start_error": "Oltava alueen aloitusarvoa pienempi",
|
"lower_range_start_error": "Oltava alueen aloitusarvoa pienempi",
|
||||||
"greater_range_start_error": "Oltava alueen aloitusarvoa suurempi",
|
"greater_range_start_error": "Oltava alueen aloitusarvoa suurempi",
|
||||||
"greater_range_end_error": "Oltava alueen päätösarvoa pienempi",
|
|
||||||
"subnet_error": "Osoitteiden tulee olla yhdessä aliverkossa",
|
"subnet_error": "Osoitteiden tulee olla yhdessä aliverkossa",
|
||||||
"gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
|
"gateway_or_subnet_invalid": "Virheellinen aliverkon peite",
|
||||||
"dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",
|
"dhcp_form_gateway_input": "Yhdyskäytävän IP-osoite",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Paramètres IPv6 du DHCP",
|
"dhcp_ipv6_settings": "Paramètres IPv6 du DHCP",
|
||||||
"form_error_required": "Champ requis",
|
"form_error_required": "Champ requis",
|
||||||
"form_error_ip4_format": "Adresse IPv4 invalide",
|
"form_error_ip4_format": "Adresse IPv4 invalide",
|
||||||
"form_error_ip4_range_start_format": "Adresse de début de plage IPv4 incorrecte",
|
|
||||||
"form_error_ip4_range_end_format": "Adresse de fin de plage IPv4 incorrecte",
|
|
||||||
"form_error_ip4_gateway_format": "Adresse de passerelle IPv4 invalide",
|
"form_error_ip4_gateway_format": "Adresse de passerelle IPv4 invalide",
|
||||||
"form_error_ip6_format": "Adresse IPv6 invalide",
|
"form_error_ip6_format": "Adresse IPv6 invalide",
|
||||||
"form_error_ip_format": "Adresse IP invalide",
|
"form_error_ip_format": "Adresse IP invalide",
|
||||||
@@ -51,9 +49,8 @@
|
|||||||
"out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »",
|
"out_of_range_error": "Doit être hors plage « {{start}} » - « {{end}} »",
|
||||||
"lower_range_start_error": "Doit être inférieur au début de plage",
|
"lower_range_start_error": "Doit être inférieur au début de plage",
|
||||||
"greater_range_start_error": "Doit être supérieur au début de plage",
|
"greater_range_start_error": "Doit être supérieur au début de plage",
|
||||||
"greater_range_end_error": "Doit être supérieur à la fin de plage",
|
|
||||||
"subnet_error": "Les adresses doivent être dans le même sous-réseau",
|
"subnet_error": "Les adresses doivent être dans le même sous-réseau",
|
||||||
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide",
|
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide.",
|
||||||
"dhcp_form_gateway_input": "IP de la passerelle",
|
"dhcp_form_gateway_input": "IP de la passerelle",
|
||||||
"dhcp_form_subnet_input": "Masque de sous-réseau",
|
"dhcp_form_subnet_input": "Masque de sous-réseau",
|
||||||
"dhcp_form_range_title": "Rangée des adresses IP",
|
"dhcp_form_range_title": "Rangée des adresses IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
||||||
"form_error_required": "Obavezno polje",
|
"form_error_required": "Obavezno polje",
|
||||||
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
||||||
"form_error_ip4_range_start_format": "Nepravilan početak ranga IPv4 adresa",
|
|
||||||
"form_error_ip4_range_end_format": "Nepravilan kraj ranga IPv4 adresa",
|
|
||||||
"form_error_ip4_gateway_format": "Nepravilna IPV4 adresa čvora",
|
"form_error_ip4_gateway_format": "Nepravilna IPV4 adresa čvora",
|
||||||
"form_error_ip6_format": "Nevažeći IPv6 adresa",
|
"form_error_ip6_format": "Nevažeći IPv6 adresa",
|
||||||
"form_error_ip_format": "Nepravilna IP adresa",
|
"form_error_ip_format": "Nepravilna IP adresa",
|
||||||
@@ -51,9 +49,8 @@
|
|||||||
"out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Mora biti izvan ranga \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Mora biti niže od početnog ranga",
|
"lower_range_start_error": "Mora biti niže od početnog ranga",
|
||||||
"greater_range_start_error": "Mora biti veće od krajnjeg ranga",
|
"greater_range_start_error": "Mora biti veće od krajnjeg ranga",
|
||||||
"greater_range_end_error": "Mora biti veće od krajnjeg ranga",
|
|
||||||
"subnet_error": "Adrese moraju biti iz iste podmreže",
|
"subnet_error": "Adrese moraju biti iz iste podmreže",
|
||||||
"gateway_or_subnet_invalid": "Maska podmreže je neprvilna",
|
"gateway_or_subnet_invalid": "Nevažeća podmrežna maska",
|
||||||
"dhcp_form_gateway_input": "Gateway IP",
|
"dhcp_form_gateway_input": "Gateway IP",
|
||||||
"dhcp_form_subnet_input": "Subnet maskiranje",
|
"dhcp_form_subnet_input": "Subnet maskiranje",
|
||||||
"dhcp_form_range_title": "Raspon IP adresa",
|
"dhcp_form_range_title": "Raspon IP adresa",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 Beállítások",
|
"dhcp_ipv6_settings": "DHCP IPv6 Beállítások",
|
||||||
"form_error_required": "Kötelező mező",
|
"form_error_required": "Kötelező mező",
|
||||||
"form_error_ip4_format": "Érvénytelen IPv4 cím",
|
"form_error_ip4_format": "Érvénytelen IPv4 cím",
|
||||||
"form_error_ip4_range_start_format": "Érvénytelen IPv4-cím a tartomány kezdetéhez",
|
|
||||||
"form_error_ip4_range_end_format": "Érvénytelen IPv4-cím a tartomány végén",
|
|
||||||
"form_error_ip4_gateway_format": "Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva",
|
"form_error_ip4_gateway_format": "Az átjáróhoz (gateway) érvénytelen IPv4 cím lett megadva",
|
||||||
"form_error_ip6_format": "Érvénytelen IPv6 cím",
|
"form_error_ip6_format": "Érvénytelen IPv6 cím",
|
||||||
"form_error_ip_format": "Érvénytelen IP-cím",
|
"form_error_ip_format": "Érvénytelen IP-cím",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "A következő tartományon kívül legyen: \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "A következő tartományon kívül legyen: \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Kisebb legyen, mint a tartomány kezdete",
|
"lower_range_start_error": "Kisebb legyen, mint a tartomány kezdete",
|
||||||
"greater_range_start_error": "Nagyobbnak kell lennie, mint a tartomány kezdete",
|
"greater_range_start_error": "Nagyobbnak kell lennie, mint a tartomány kezdete",
|
||||||
"greater_range_end_error": "Nagyobb legyen, mint a tartomány vége",
|
|
||||||
"subnet_error": "A címeknek egy alhálózatban kell lenniük",
|
"subnet_error": "A címeknek egy alhálózatban kell lenniük",
|
||||||
"gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
|
"gateway_or_subnet_invalid": "Az alhálózati maszk érvénytelen",
|
||||||
"dhcp_form_gateway_input": "Átjáró IP",
|
"dhcp_form_gateway_input": "Átjáró IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Pengaturan DHCP IPv6",
|
"dhcp_ipv6_settings": "Pengaturan DHCP IPv6",
|
||||||
"form_error_required": "Kolom yang harus diisi",
|
"form_error_required": "Kolom yang harus diisi",
|
||||||
"form_error_ip4_format": "Alamat IPv4 tidak valid",
|
"form_error_ip4_format": "Alamat IPv4 tidak valid",
|
||||||
"form_error_ip4_range_start_format": "Alamat IPv4 tidak valid dari rentang awal",
|
|
||||||
"form_error_ip4_range_end_format": "Alamat IPv4 tidak valid dari rentang akhir",
|
|
||||||
"form_error_ip4_gateway_format": "Alamat IPv4 gateway tidak valid",
|
"form_error_ip4_gateway_format": "Alamat IPv4 gateway tidak valid",
|
||||||
"form_error_ip6_format": "Alamat IPv6 tidak valid",
|
"form_error_ip6_format": "Alamat IPv6 tidak valid",
|
||||||
"form_error_ip_format": "Alamat IP tidak valid",
|
"form_error_ip_format": "Alamat IP tidak valid",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Harus di luar rentang \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Harus lebih rendah dari rentang awal",
|
"lower_range_start_error": "Harus lebih rendah dari rentang awal",
|
||||||
"greater_range_start_error": "Harus lebih besar dari rentang awal",
|
"greater_range_start_error": "Harus lebih besar dari rentang awal",
|
||||||
"greater_range_end_error": "Harus lebih besar dari rentang akhir",
|
|
||||||
"subnet_error": "Alamat harus dalam satu subnet",
|
"subnet_error": "Alamat harus dalam satu subnet",
|
||||||
"gateway_or_subnet_invalid": "Subnet mask tidak valid",
|
"gateway_or_subnet_invalid": "Subnet mask tidak valid",
|
||||||
"dhcp_form_gateway_input": "IP gateway",
|
"dhcp_form_gateway_input": "IP gateway",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
|
"dhcp_ipv6_settings": "Impostazioni DHCP IPv6",
|
||||||
"form_error_required": "Campo richiesto",
|
"form_error_required": "Campo richiesto",
|
||||||
"form_error_ip4_format": "Indirizzo IPv4 non valido",
|
"form_error_ip4_format": "Indirizzo IPv4 non valido",
|
||||||
"form_error_ip4_range_start_format": "Indirizzo IPV4 non valido dell'intervallo iniziale",
|
|
||||||
"form_error_ip4_range_end_format": "Indirizzo IPV4 non valido dell'intervallo finale",
|
|
||||||
"form_error_ip4_gateway_format": "Indirizzo gateway IPv4 non valido",
|
"form_error_ip4_gateway_format": "Indirizzo gateway IPv4 non valido",
|
||||||
"form_error_ip6_format": "Indirizzo IPv6 non valido",
|
"form_error_ip6_format": "Indirizzo IPv6 non valido",
|
||||||
"form_error_ip_format": "Indirizzo IP non valido",
|
"form_error_ip_format": "Indirizzo IP non valido",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Deve essere fuori intervallo \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Deve essere inferiore dell'intervallo di inizio",
|
"lower_range_start_error": "Deve essere inferiore dell'intervallo di inizio",
|
||||||
"greater_range_start_error": "Deve essere maggiore dell'intervallo di inizio",
|
"greater_range_start_error": "Deve essere maggiore dell'intervallo di inizio",
|
||||||
"greater_range_end_error": "Deve essere maggiore dell'intervallo di fine",
|
|
||||||
"subnet_error": "Gli indirizzi devono trovarsi in una sottorete",
|
"subnet_error": "Gli indirizzi devono trovarsi in una sottorete",
|
||||||
"gateway_or_subnet_invalid": "Maschera di sottorete non valida",
|
"gateway_or_subnet_invalid": "Maschera di sottorete non valida",
|
||||||
"dhcp_form_gateway_input": "IP Gateway",
|
"dhcp_form_gateway_input": "IP Gateway",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
||||||
"form_error_required": "必須項目です",
|
"form_error_required": "必須項目です",
|
||||||
"form_error_ip4_format": "IPv4アドレスが無効です",
|
"form_error_ip4_format": "IPv4アドレスが無効です",
|
||||||
"form_error_ip4_range_start_format": "範囲開始のIPv4アドレスが無効です",
|
|
||||||
"form_error_ip4_range_end_format": "範囲終了のIPv4アドレスが無効です",
|
|
||||||
"form_error_ip4_gateway_format": "ゲートウェイのIPv4アドレスが無効です",
|
"form_error_ip4_gateway_format": "ゲートウェイのIPv4アドレスが無効です",
|
||||||
"form_error_ip6_format": "IPv6アドレスが無効です",
|
"form_error_ip6_format": "IPv6アドレスが無効です",
|
||||||
"form_error_ip_format": "IPアドレスが無効です",
|
"form_error_ip_format": "IPアドレスが無効です",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "\"{{start}}\"〜\"{{end}}\" の範囲外である必要があります",
|
"out_of_range_error": "\"{{start}}\"〜\"{{end}}\" の範囲外である必要があります",
|
||||||
"lower_range_start_error": "範囲開始よりも低い値である必要があります",
|
"lower_range_start_error": "範囲開始よりも低い値である必要があります",
|
||||||
"greater_range_start_error": "範囲開始値より大きい値でなければなりません",
|
"greater_range_start_error": "範囲開始値より大きい値でなければなりません",
|
||||||
"greater_range_end_error": "範囲終了値より大きい値でなければなりません",
|
|
||||||
"subnet_error": "両アドレスが同じサブネット内にある必要があります",
|
"subnet_error": "両アドレスが同じサブネット内にある必要があります",
|
||||||
"gateway_or_subnet_invalid": "サブネットマスクが無効です",
|
"gateway_or_subnet_invalid": "サブネットマスクが無効です",
|
||||||
"dhcp_form_gateway_input": "ゲートウェイIP",
|
"dhcp_form_gateway_input": "ゲートウェイIP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 설정",
|
"dhcp_ipv6_settings": "DHCP IPv6 설정",
|
||||||
"form_error_required": "필수 영역",
|
"form_error_required": "필수 영역",
|
||||||
"form_error_ip4_format": "잘못된 IPv4 형식",
|
"form_error_ip4_format": "잘못된 IPv4 형식",
|
||||||
"form_error_ip4_range_start_format": "잘못된 범위 시작 IPv4 형식",
|
|
||||||
"form_error_ip4_range_end_format": "잘못된 범위 종료 IPv4 형식",
|
|
||||||
"form_error_ip4_gateway_format": "잘못된 게이트웨이 IPv4 형식",
|
"form_error_ip4_gateway_format": "잘못된 게이트웨이 IPv4 형식",
|
||||||
"form_error_ip6_format": "잘못된 IPv6 주소",
|
"form_error_ip6_format": "잘못된 IPv6 주소",
|
||||||
"form_error_ip_format": "잘못된 IP 주소",
|
"form_error_ip_format": "잘못된 IP 주소",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "'{{start}}'-'{{end}}' 범위 밖이어야 합니다",
|
"out_of_range_error": "'{{start}}'-'{{end}}' 범위 밖이어야 합니다",
|
||||||
"lower_range_start_error": "범위 시작보다 작은 값이어야 합니다",
|
"lower_range_start_error": "범위 시작보다 작은 값이어야 합니다",
|
||||||
"greater_range_start_error": "범위 시작보다 큰 값이어야 합니다",
|
"greater_range_start_error": "범위 시작보다 큰 값이어야 합니다",
|
||||||
"greater_range_end_error": "범위 종료보다 큰 값이어야 합니다",
|
|
||||||
"subnet_error": "주소는 하나의 서브넷에 있어야 합니다",
|
"subnet_error": "주소는 하나의 서브넷에 있어야 합니다",
|
||||||
"gateway_or_subnet_invalid": "잘못된 서브넷 마스크",
|
"gateway_or_subnet_invalid": "잘못된 서브넷 마스크",
|
||||||
"dhcp_form_gateway_input": "게이트웨이 IP",
|
"dhcp_form_gateway_input": "게이트웨이 IP",
|
||||||
@@ -223,7 +220,7 @@
|
|||||||
"example_upstream_tcp_hostname": "일반 DNS (TCP를 통한, 호스트명);",
|
"example_upstream_tcp_hostname": "일반 DNS (TCP를 통한, 호스트명);",
|
||||||
"all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다",
|
"all_lists_up_to_date_toast": "모든 리스트가 이미 최신입니다",
|
||||||
"updated_upstream_dns_toast": "업스트림 서버가 성공적으로 저장되었습니다",
|
"updated_upstream_dns_toast": "업스트림 서버가 성공적으로 저장되었습니다",
|
||||||
"dns_test_ok_toast": "특정 DNS 서버들은 정상적으로 동작 중입니다",
|
"dns_test_ok_toast": "지정된 DNS 서버가 올바르게 작동하고 있습니다.",
|
||||||
"dns_test_not_ok_toast": "서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요",
|
"dns_test_not_ok_toast": "서버 '{{key}}': 사용할 수 없습니다, 제대로 작성했는지 확인하세요",
|
||||||
"dns_test_warning_toast": "업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다",
|
"dns_test_warning_toast": "업스트림 '{{key}}'이(가) 테스트 요청에 응답하지 않으며 제대로 작동하지 않을 수 있습니다",
|
||||||
"unblock": "차단 해제",
|
"unblock": "차단 해제",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 instellingen",
|
"dhcp_ipv6_settings": "DHCP IPv6 instellingen",
|
||||||
"form_error_required": "Vereist veld",
|
"form_error_required": "Vereist veld",
|
||||||
"form_error_ip4_format": "Ongeldig IPv4-adres",
|
"form_error_ip4_format": "Ongeldig IPv4-adres",
|
||||||
"form_error_ip4_range_start_format": "Ongeldig IPv4-adres start bereik",
|
|
||||||
"form_error_ip4_range_end_format": "Ongeldig IPv4-adres einde bereik",
|
|
||||||
"form_error_ip4_gateway_format": "Ongeldig IPv4-adres van de gateway",
|
"form_error_ip4_gateway_format": "Ongeldig IPv4-adres van de gateway",
|
||||||
"form_error_ip6_format": "Ongeldig IPv6-adres",
|
"form_error_ip6_format": "Ongeldig IPv6-adres",
|
||||||
"form_error_ip_format": "Ongeldig IP-adres",
|
"form_error_ip_format": "Ongeldig IP-adres",
|
||||||
@@ -51,9 +49,8 @@
|
|||||||
"out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Moet buiten bereik zijn \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Moet lager zijn dan begin reeks",
|
"lower_range_start_error": "Moet lager zijn dan begin reeks",
|
||||||
"greater_range_start_error": "Moet groter zijn dan begin reeks",
|
"greater_range_start_error": "Moet groter zijn dan begin reeks",
|
||||||
"greater_range_end_error": "Moet groter zijn dan einde reeks",
|
|
||||||
"subnet_error": "Adressen moeten in één subnet vallen",
|
"subnet_error": "Adressen moeten in één subnet vallen",
|
||||||
"gateway_or_subnet_invalid": "Subnetmasker ongeldig",
|
"gateway_or_subnet_invalid": "Ongeldig subnetmasker",
|
||||||
"dhcp_form_gateway_input": "Gateway IP",
|
"dhcp_form_gateway_input": "Gateway IP",
|
||||||
"dhcp_form_subnet_input": "Subnet mask",
|
"dhcp_form_subnet_input": "Subnet mask",
|
||||||
"dhcp_form_range_title": "Bereik van IP adressen",
|
"dhcp_form_range_title": "Bereik van IP adressen",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6",
|
"dhcp_ipv6_settings": "Ustawienia serwera DHCP IPv6",
|
||||||
"form_error_required": "Pole wymagane",
|
"form_error_required": "Pole wymagane",
|
||||||
"form_error_ip4_format": "Nieprawidłowy adres IPv4",
|
"form_error_ip4_format": "Nieprawidłowy adres IPv4",
|
||||||
"form_error_ip4_range_start_format": "Nieprawidłowy adres IPv4 początku zakresu",
|
|
||||||
"form_error_ip4_range_end_format": "Nieprawidłowy adres IPv4 końca zakresu",
|
|
||||||
"form_error_ip4_gateway_format": "Nieprawidłowy adres IPv4 bramy",
|
"form_error_ip4_gateway_format": "Nieprawidłowy adres IPv4 bramy",
|
||||||
"form_error_ip6_format": "Nieprawidłowy adres IPv6",
|
"form_error_ip6_format": "Nieprawidłowy adres IPv6",
|
||||||
"form_error_ip_format": "Nieprawidłowy adres IP",
|
"form_error_ip_format": "Nieprawidłowy adres IP",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Musi być spoza zakresu \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Musi być niższy niż początek zakresu",
|
"lower_range_start_error": "Musi być niższy niż początek zakresu",
|
||||||
"greater_range_start_error": "Musi być większy niż początek zakresu",
|
"greater_range_start_error": "Musi być większy niż początek zakresu",
|
||||||
"greater_range_end_error": "Musi być większy niż koniec zakresu",
|
|
||||||
"subnet_error": "Adresy muszą należeć do jednej podsieci",
|
"subnet_error": "Adresy muszą należeć do jednej podsieci",
|
||||||
"gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
|
"gateway_or_subnet_invalid": "Nieprawidłowa maska podsieci",
|
||||||
"dhcp_form_gateway_input": "Adres IP bramy",
|
"dhcp_form_gateway_input": "Adres IP bramy",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Configurações DHCP IPv6",
|
"dhcp_ipv6_settings": "Configurações DHCP IPv6",
|
||||||
"form_error_required": "Campo obrigatório",
|
"form_error_required": "Campo obrigatório",
|
||||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||||
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
|
|
||||||
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido.",
|
|
||||||
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
||||||
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
||||||
"form_error_ip_format": "Endereço de IP inválido",
|
"form_error_ip_format": "Endereço de IP inválido",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
||||||
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
||||||
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
|
|
||||||
"subnet_error": "Endereços devem estar em uma sub-rede",
|
"subnet_error": "Endereços devem estar em uma sub-rede",
|
||||||
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
||||||
"dhcp_form_gateway_input": "IP do gateway",
|
"dhcp_form_gateway_input": "IP do gateway",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Definições DHCP IPv6",
|
"dhcp_ipv6_settings": "Definições DHCP IPv6",
|
||||||
"form_error_required": "Campo obrigatório",
|
"form_error_required": "Campo obrigatório",
|
||||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||||
"form_error_ip4_range_start_format": "Endereço IPv4 de início de intervalo inválido",
|
|
||||||
"form_error_ip4_range_end_format": "Endereço IPv4 de fim de intervalo inválido",
|
|
||||||
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
"form_error_ip4_gateway_format": "Endereço IPv4 de gateway inválido",
|
||||||
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
"form_error_ip6_format": "Endereço de IPv6 inválido",
|
||||||
"form_error_ip_format": "Endereço de email inválido",
|
"form_error_ip_format": "Endereço de email inválido",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Deve estar fora do intervalo \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
"lower_range_start_error": "Deve ser inferior ao início do intervalo",
|
||||||
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
"greater_range_start_error": "Deve ser maior que o início do intervalo",
|
||||||
"greater_range_end_error": "Deve ser maior que o fim do intervalo",
|
|
||||||
"subnet_error": "Os endereços devem estar em uma sub-rede",
|
"subnet_error": "Os endereços devem estar em uma sub-rede",
|
||||||
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
||||||
"dhcp_form_gateway_input": "IP do gateway",
|
"dhcp_form_gateway_input": "IP do gateway",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Setări DHCP IPv6",
|
"dhcp_ipv6_settings": "Setări DHCP IPv6",
|
||||||
"form_error_required": "Câmp obligatoriu",
|
"form_error_required": "Câmp obligatoriu",
|
||||||
"form_error_ip4_format": "Adresă IPv4 nevalidă",
|
"form_error_ip4_format": "Adresă IPv4 nevalidă",
|
||||||
"form_error_ip4_range_start_format": "Adresă IPv4 nevalidă pentru începutul intervalului",
|
|
||||||
"form_error_ip4_range_end_format": "Adresă IPv4 nevalidă a sfârșitului intervalului",
|
|
||||||
"form_error_ip4_gateway_format": "Adresă IPv4 nevalidă a gateway-ului",
|
"form_error_ip4_gateway_format": "Adresă IPv4 nevalidă a gateway-ului",
|
||||||
"form_error_ip6_format": "Adresa IPv6 nevalidă",
|
"form_error_ip6_format": "Adresa IPv6 nevalidă",
|
||||||
"form_error_ip_format": "Adresă IP nevalidă",
|
"form_error_ip_format": "Adresă IP nevalidă",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”",
|
"out_of_range_error": "Trebuie să fie în afara intervalului „{{start}}”-„{{end}}”",
|
||||||
"lower_range_start_error": "Trebuie să fie mai mică decât începutul intervalului",
|
"lower_range_start_error": "Trebuie să fie mai mică decât începutul intervalului",
|
||||||
"greater_range_start_error": "Trebuie să fie mai mare decât începutul intervalului",
|
"greater_range_start_error": "Trebuie să fie mai mare decât începutul intervalului",
|
||||||
"greater_range_end_error": "Trebuie să fie mai mare decât sfârșitul intervalului",
|
|
||||||
"subnet_error": "Adresele trebuie să fie în aceeași subrețea",
|
"subnet_error": "Adresele trebuie să fie în aceeași subrețea",
|
||||||
"gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
|
"gateway_or_subnet_invalid": "Mască de subrețea nevalidă",
|
||||||
"dhcp_form_gateway_input": "IP Gateway",
|
"dhcp_form_gateway_input": "IP Gateway",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Настройки DHCP IPv6",
|
"dhcp_ipv6_settings": "Настройки DHCP IPv6",
|
||||||
"form_error_required": "Обязательное поле",
|
"form_error_required": "Обязательное поле",
|
||||||
"form_error_ip4_format": "Некорректный IPv4-адрес",
|
"form_error_ip4_format": "Некорректный IPv4-адрес",
|
||||||
"form_error_ip4_range_start_format": "Некорректный IPv4-адрес начала диапазона",
|
|
||||||
"form_error_ip4_range_end_format": "Некорректный IPv4-адрес конца диапазона",
|
|
||||||
"form_error_ip4_gateway_format": "Некорректный IPv4-адрес шлюза",
|
"form_error_ip4_gateway_format": "Некорректный IPv4-адрес шлюза",
|
||||||
"form_error_ip6_format": "Некорректный IPv6-адрес",
|
"form_error_ip6_format": "Некорректный IPv6-адрес",
|
||||||
"form_error_ip_format": "Некорректный IP-адрес",
|
"form_error_ip_format": "Некорректный IP-адрес",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Должно быть вне диапазона «{{start}}»-«{{end}}»",
|
"out_of_range_error": "Должно быть вне диапазона «{{start}}»-«{{end}}»",
|
||||||
"lower_range_start_error": "Должно быть меньше начала диапазона",
|
"lower_range_start_error": "Должно быть меньше начала диапазона",
|
||||||
"greater_range_start_error": "Должно быть больше начала диапазона",
|
"greater_range_start_error": "Должно быть больше начала диапазона",
|
||||||
"greater_range_end_error": "Должно быть больше конца диапазона",
|
|
||||||
"subnet_error": "Адреса должны быть внутри одной подсети",
|
"subnet_error": "Адреса должны быть внутри одной подсети",
|
||||||
"gateway_or_subnet_invalid": "Некорректная маска подсети",
|
"gateway_or_subnet_invalid": "Некорректная маска подсети",
|
||||||
"dhcp_form_gateway_input": "IP-адрес шлюза",
|
"dhcp_form_gateway_input": "IP-адрес шлюза",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
|
"dhcp_ipv6_settings": "Nastavenia DHCP IPv6",
|
||||||
"form_error_required": "Povinná položka.",
|
"form_error_required": "Povinná položka.",
|
||||||
"form_error_ip4_format": "Neplatná IPv4 adresa",
|
"form_error_ip4_format": "Neplatná IPv4 adresa",
|
||||||
"form_error_ip4_range_start_format": "Neplatný začiatok rozsahu IPv4 formátu",
|
|
||||||
"form_error_ip4_range_end_format": "Neplatný koniec rozsahu IPv4 formátu",
|
|
||||||
"form_error_ip4_gateway_format": "Neplatná IPv4 adresa brány",
|
"form_error_ip4_gateway_format": "Neplatná IPv4 adresa brány",
|
||||||
"form_error_ip6_format": "Neplatná IPv6 adresa",
|
"form_error_ip6_format": "Neplatná IPv6 adresa",
|
||||||
"form_error_ip_format": "Neplatná IP adresa",
|
"form_error_ip_format": "Neplatná IP adresa",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Musí byť mimo rozsahu \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Musí byť nižšie ako začiatok rozsahu",
|
"lower_range_start_error": "Musí byť nižšie ako začiatok rozsahu",
|
||||||
"greater_range_start_error": "Musí byť väčšie ako začiatok rozsahu",
|
"greater_range_start_error": "Musí byť väčšie ako začiatok rozsahu",
|
||||||
"greater_range_end_error": "Musí byť väčšie ako koniec rozsahu",
|
|
||||||
"subnet_error": "Adresy musia byť v spoločnej podsieti",
|
"subnet_error": "Adresy musia byť v spoločnej podsieti",
|
||||||
"gateway_or_subnet_invalid": "Maska podsiete je neplatná",
|
"gateway_or_subnet_invalid": "Maska podsiete je neplatná",
|
||||||
"dhcp_form_gateway_input": "IP brána",
|
"dhcp_form_gateway_input": "IP brána",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Nastavitve DHCP IPv6",
|
"dhcp_ipv6_settings": "Nastavitve DHCP IPv6",
|
||||||
"form_error_required": "Zahtevano polje.",
|
"form_error_required": "Zahtevano polje.",
|
||||||
"form_error_ip4_format": "Neveljaven naslov IPv4.",
|
"form_error_ip4_format": "Neveljaven naslov IPv4.",
|
||||||
"form_error_ip4_range_start_format": "Neveljaven začetek razpona naslova IPv4",
|
|
||||||
"form_error_ip4_range_end_format": "Neveljaven konec razpona naslova IPv4",
|
|
||||||
"form_error_ip4_gateway_format": "Neveljaven naslov IPv4 prehoda",
|
"form_error_ip4_gateway_format": "Neveljaven naslov IPv4 prehoda",
|
||||||
"form_error_ip6_format": "Neveljaven naslov IPv6",
|
"form_error_ip6_format": "Neveljaven naslov IPv6",
|
||||||
"form_error_ip_format": "Neveljaven naslov IP",
|
"form_error_ip_format": "Neveljaven naslov IP",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Mora biti izven razpona \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Mora biti manjši od začetka razpona",
|
"lower_range_start_error": "Mora biti manjši od začetka razpona",
|
||||||
"greater_range_start_error": "Mora biti večji od začetka razpona",
|
"greater_range_start_error": "Mora biti večji od začetka razpona",
|
||||||
"greater_range_end_error": "Mora biti večji od konca razpona",
|
|
||||||
"subnet_error": "Naslovi morajo biti v enem podomrežju",
|
"subnet_error": "Naslovi morajo biti v enem podomrežju",
|
||||||
"gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
|
"gateway_or_subnet_invalid": "Maska podomrežja ni veljavna",
|
||||||
"dhcp_form_gateway_input": "IP prehoda",
|
"dhcp_form_gateway_input": "IP prehoda",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
"dhcp_ipv6_settings": "DHCP IPv6 postavke",
|
||||||
"form_error_required": "Obavezno polje",
|
"form_error_required": "Obavezno polje",
|
||||||
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
"form_error_ip4_format": "Nevažeća IPv4 adresa",
|
||||||
"form_error_ip4_range_start_format": "Nevažeća IPv4 addresa početnog opsega",
|
|
||||||
"form_error_ip4_range_end_format": "Nevažeća IPv4 addresa završnog opsega",
|
|
||||||
"form_error_ip4_gateway_format": "Nevažeća IPv4 addresa prozala",
|
"form_error_ip4_gateway_format": "Nevažeća IPv4 addresa prozala",
|
||||||
"form_error_ip6_format": "Nevažeća IPv6 adresa",
|
"form_error_ip6_format": "Nevažeća IPv6 adresa",
|
||||||
"form_error_ip_format": "Nevažeća IP adresa",
|
"form_error_ip_format": "Nevažeća IP adresa",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Mora biti izvan opsega \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Mora biti manje od početnog opsega",
|
"lower_range_start_error": "Mora biti manje od početnog opsega",
|
||||||
"greater_range_start_error": "Mora biti veće od početnog opsega",
|
"greater_range_start_error": "Mora biti veće od početnog opsega",
|
||||||
"greater_range_end_error": "Mora biti veće od završnog opsega",
|
|
||||||
"subnet_error": "Asrese moraju biti u jednoj subnet",
|
"subnet_error": "Asrese moraju biti u jednoj subnet",
|
||||||
"gateway_or_subnet_invalid": "Subnet mask nevažeća",
|
"gateway_or_subnet_invalid": "Subnet mask nevažeća",
|
||||||
"dhcp_form_gateway_input": "IP mrežnog prolaza",
|
"dhcp_form_gateway_input": "IP mrežnog prolaza",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 inställningar",
|
"dhcp_ipv6_settings": "DHCP IPv6 inställningar",
|
||||||
"form_error_required": "Obligatoriskt fält",
|
"form_error_required": "Obligatoriskt fält",
|
||||||
"form_error_ip4_format": "Ogiltig IPv4-adress",
|
"form_error_ip4_format": "Ogiltig IPv4-adress",
|
||||||
"form_error_ip4_range_start_format": "Ogiltig IPv4-adress för starten av intervallet",
|
|
||||||
"form_error_ip4_range_end_format": "Ogiltig IPv4-adress för slutet av intervallet",
|
|
||||||
"form_error_ip4_gateway_format": "Ogiltig IPv4 adress för gatewayen",
|
"form_error_ip4_gateway_format": "Ogiltig IPv4 adress för gatewayen",
|
||||||
"form_error_ip6_format": "Ogiltig IPv6-adress",
|
"form_error_ip6_format": "Ogiltig IPv6-adress",
|
||||||
"form_error_ip_format": "Ogiltig IP-adress",
|
"form_error_ip_format": "Ogiltig IP-adress",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Måste vara utanför intervallet \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Måste vara utanför intervallet \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Måste vara lägre än starten på intervallet",
|
"lower_range_start_error": "Måste vara lägre än starten på intervallet",
|
||||||
"greater_range_start_error": "Måste vara högre än starten på intervallet",
|
"greater_range_start_error": "Måste vara högre än starten på intervallet",
|
||||||
"greater_range_end_error": "Måste vara större än intervallets slut",
|
|
||||||
"subnet_error": "Adresser måste finnas i ett subnät",
|
"subnet_error": "Adresser måste finnas i ett subnät",
|
||||||
"gateway_or_subnet_invalid": "Subnätmask ogiltig",
|
"gateway_or_subnet_invalid": "Subnätmask ogiltig",
|
||||||
"dhcp_form_gateway_input": "Gateway-IP",
|
"dhcp_form_gateway_input": "Gateway-IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
"dhcp_ipv6_settings": "DHCP IPv6 Ayarları",
|
||||||
"form_error_required": "Gerekli alan",
|
"form_error_required": "Gerekli alan",
|
||||||
"form_error_ip4_format": "Geçersiz IPv4 adresi",
|
"form_error_ip4_format": "Geçersiz IPv4 adresi",
|
||||||
"form_error_ip4_range_start_format": "Geçersiz başlangıç aralığı IPv4 biçimi",
|
|
||||||
"form_error_ip4_range_end_format": "Geçersiz bitiş aralığı IPv4 adresi",
|
|
||||||
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
|
"form_error_ip4_gateway_format": "Geçersiz ağ geçidi IPv4 adresi",
|
||||||
"form_error_ip6_format": "Geçersiz IPv6 adresi",
|
"form_error_ip6_format": "Geçersiz IPv6 adresi",
|
||||||
"form_error_ip_format": "Geçersiz IP adresi",
|
"form_error_ip_format": "Geçersiz IP adresi",
|
||||||
@@ -51,9 +49,8 @@
|
|||||||
"out_of_range_error": "\"{{start}}\"-\"{{end}}\" aralığının dışında olmalıdır",
|
"out_of_range_error": "\"{{start}}\"-\"{{end}}\" aralığının dışında olmalıdır",
|
||||||
"lower_range_start_error": "Başlangıç aralığından daha düşük olmalıdır",
|
"lower_range_start_error": "Başlangıç aralığından daha düşük olmalıdır",
|
||||||
"greater_range_start_error": "Başlangıç aralığından daha büyük olmalıdır",
|
"greater_range_start_error": "Başlangıç aralığından daha büyük olmalıdır",
|
||||||
"greater_range_end_error": "Bitiş aralığından daha büyük olmalıdır",
|
|
||||||
"subnet_error": "Adresler bir alt ağda olmalıdır",
|
"subnet_error": "Adresler bir alt ağda olmalıdır",
|
||||||
"gateway_or_subnet_invalid": "Alt ağ maskesi geçersiz",
|
"gateway_or_subnet_invalid": "Geçersiz alt ağ maskesi",
|
||||||
"dhcp_form_gateway_input": "Ağ geçidi IP",
|
"dhcp_form_gateway_input": "Ağ geçidi IP",
|
||||||
"dhcp_form_subnet_input": "Alt ağ maskesi",
|
"dhcp_form_subnet_input": "Alt ağ maskesi",
|
||||||
"dhcp_form_range_title": "IP adresi aralığı",
|
"dhcp_form_range_title": "IP adresi aralığı",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Налаштування DHCP IPv6",
|
"dhcp_ipv6_settings": "Налаштування DHCP IPv6",
|
||||||
"form_error_required": "Обов'язкове поле",
|
"form_error_required": "Обов'язкове поле",
|
||||||
"form_error_ip4_format": "Неправильна IPv4-адреса",
|
"form_error_ip4_format": "Неправильна IPv4-адреса",
|
||||||
"form_error_ip4_range_start_format": "Неправильна IPv4-адреса початку діапазону",
|
|
||||||
"form_error_ip4_range_end_format": "Неправильна IPv4-адреса кінця діапазону",
|
|
||||||
"form_error_ip4_gateway_format": "Неправильна IPv4-адреса шлюзу",
|
"form_error_ip4_gateway_format": "Неправильна IPv4-адреса шлюзу",
|
||||||
"form_error_ip6_format": "Неправильна IPv6-адреса",
|
"form_error_ip6_format": "Неправильна IPv6-адреса",
|
||||||
"form_error_ip_format": "Неправильна IP-адреса",
|
"form_error_ip_format": "Неправильна IP-адреса",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Не повинна бути в діапазоні «{{start}}»−«{{end}}»",
|
"out_of_range_error": "Не повинна бути в діапазоні «{{start}}»−«{{end}}»",
|
||||||
"lower_range_start_error": "Має бути меншим за початкову адресу",
|
"lower_range_start_error": "Має бути меншим за початкову адресу",
|
||||||
"greater_range_start_error": "Має бути більшим за початкову адресу",
|
"greater_range_start_error": "Має бути більшим за початкову адресу",
|
||||||
"greater_range_end_error": "Має бути більшим за кінцеву адресу",
|
|
||||||
"subnet_error": "Адреси повинні бути в одній підмережі",
|
"subnet_error": "Адреси повинні бути в одній підмережі",
|
||||||
"gateway_or_subnet_invalid": "Неправильна маска підмережі",
|
"gateway_or_subnet_invalid": "Неправильна маска підмережі",
|
||||||
"dhcp_form_gateway_input": "IP-адреса шлюзу",
|
"dhcp_form_gateway_input": "IP-адреса шлюзу",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "Cài đặt DHCP IPv6",
|
"dhcp_ipv6_settings": "Cài đặt DHCP IPv6",
|
||||||
"form_error_required": "Trường bắt buộc",
|
"form_error_required": "Trường bắt buộc",
|
||||||
"form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ",
|
"form_error_ip4_format": "Địa chỉ IPv4 không hợp lệ",
|
||||||
"form_error_ip4_range_start_format": "Địa chỉ IPv4 không hợp lệ của phạm vi bắt đầu",
|
|
||||||
"form_error_ip4_range_end_format": "Địa chỉ IPv4 không hợp lệ của cuối phạm vi",
|
|
||||||
"form_error_ip4_gateway_format": "Địa chỉ IPv4 không hợp lệ của cổng kết nối",
|
"form_error_ip4_gateway_format": "Địa chỉ IPv4 không hợp lệ của cổng kết nối",
|
||||||
"form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ",
|
"form_error_ip6_format": "Địa chỉ IPv6 không hợp lệ",
|
||||||
"form_error_ip_format": "Địa chỉ IP không hợp lệ",
|
"form_error_ip_format": "Địa chỉ IP không hợp lệ",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "Phải nằm ngoài phạm vi \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "Phải nằm ngoài phạm vi \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "Phải thấp hơn khởi động phạm vi",
|
"lower_range_start_error": "Phải thấp hơn khởi động phạm vi",
|
||||||
"greater_range_start_error": "Phải lớn hơn khoảng bắt đầu",
|
"greater_range_start_error": "Phải lớn hơn khoảng bắt đầu",
|
||||||
"greater_range_end_error": "Phải lớn hơn phạm vi kết thúc",
|
|
||||||
"subnet_error": "Địa chỉ phải nằm trong một mạng con",
|
"subnet_error": "Địa chỉ phải nằm trong một mạng con",
|
||||||
"gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
|
"gateway_or_subnet_invalid": "Mặt nạ mạng con không hợp lệ",
|
||||||
"dhcp_form_gateway_input": "Cổng IP",
|
"dhcp_form_gateway_input": "Cổng IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6设置",
|
"dhcp_ipv6_settings": "DHCP IPv6设置",
|
||||||
"form_error_required": "必填字段",
|
"form_error_required": "必填字段",
|
||||||
"form_error_ip4_format": "无效的 IPv4 地址",
|
"form_error_ip4_format": "无效的 IPv4 地址",
|
||||||
"form_error_ip4_range_start_format": "范围起始值的 IPv4 地址无效",
|
|
||||||
"form_error_ip4_range_end_format": "范围终值的 IPv4 地址无效",
|
|
||||||
"form_error_ip4_gateway_format": "网关 IPv4 地址无效",
|
"form_error_ip4_gateway_format": "网关 IPv4 地址无效",
|
||||||
"form_error_ip6_format": "无效的 IPv6 地址",
|
"form_error_ip6_format": "无效的 IPv6 地址",
|
||||||
"form_error_ip_format": "无效的 IP 地址",
|
"form_error_ip_format": "无效的 IP 地址",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "必定超出了范围 \"{{start}}\"-\"{{end}}\"",
|
"out_of_range_error": "必定超出了范围 \"{{start}}\"-\"{{end}}\"",
|
||||||
"lower_range_start_error": "必须小于范围起始值",
|
"lower_range_start_error": "必须小于范围起始值",
|
||||||
"greater_range_start_error": "必须大于范围起始值",
|
"greater_range_start_error": "必须大于范围起始值",
|
||||||
"greater_range_end_error": "必须大于范围终值",
|
|
||||||
"subnet_error": "地址必须在一个子网内",
|
"subnet_error": "地址必须在一个子网内",
|
||||||
"gateway_or_subnet_invalid": "子网掩码无效",
|
"gateway_or_subnet_invalid": "子网掩码无效",
|
||||||
"dhcp_form_gateway_input": "网关 IP",
|
"dhcp_form_gateway_input": "网关 IP",
|
||||||
|
|||||||
@@ -37,8 +37,6 @@
|
|||||||
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
"dhcp_ipv6_settings": "DHCP IPv6 設定",
|
||||||
"form_error_required": "必填的欄位",
|
"form_error_required": "必填的欄位",
|
||||||
"form_error_ip4_format": "無效的 IPv4 位址",
|
"form_error_ip4_format": "無效的 IPv4 位址",
|
||||||
"form_error_ip4_range_start_format": "無效起始範圍的 IPv4 位址",
|
|
||||||
"form_error_ip4_range_end_format": "無效結束範圍的 IPv4 位址",
|
|
||||||
"form_error_ip4_gateway_format": "無效閘道的 IPv4 位址",
|
"form_error_ip4_gateway_format": "無效閘道的 IPv4 位址",
|
||||||
"form_error_ip6_format": "無效的 IPv6 位址",
|
"form_error_ip6_format": "無效的 IPv6 位址",
|
||||||
"form_error_ip_format": "無效的 IP 位址",
|
"form_error_ip_format": "無效的 IP 位址",
|
||||||
@@ -51,7 +49,6 @@
|
|||||||
"out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外",
|
"out_of_range_error": "必須在\"{{start}}\"-\"{{end}}\"範圍之外",
|
||||||
"lower_range_start_error": "必須低於起始範圍",
|
"lower_range_start_error": "必須低於起始範圍",
|
||||||
"greater_range_start_error": "必須大於起始範圍",
|
"greater_range_start_error": "必須大於起始範圍",
|
||||||
"greater_range_end_error": "必須大於結束範圍",
|
|
||||||
"subnet_error": "位址必須在子網路中",
|
"subnet_error": "位址必須在子網路中",
|
||||||
"gateway_or_subnet_invalid": "無效的子網路遮罩",
|
"gateway_or_subnet_invalid": "無效的子網路遮罩",
|
||||||
"dhcp_form_gateway_input": "閘道 IP",
|
"dhcp_form_gateway_input": "閘道 IP",
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
|||||||
response.certificate_chain = atob(response.certificate_chain);
|
response.certificate_chain = atob(response.certificate_chain);
|
||||||
response.private_key = atob(response.private_key);
|
response.private_key = atob(response.private_key);
|
||||||
|
|
||||||
|
if (values.enabled && values.force_https && window.location.protocol === 'http:') {
|
||||||
|
window.location.reload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
redirectToCurrentProtocol(response, httpPort);
|
||||||
|
|
||||||
const dnsStatus = await apiClient.getGlobalStatus();
|
const dnsStatus = await apiClient.getGlobalStatus();
|
||||||
if (dnsStatus) {
|
if (dnsStatus) {
|
||||||
dispatch(dnsStatusSuccess(dnsStatus));
|
dispatch(dnsStatusSuccess(dnsStatus));
|
||||||
@@ -48,7 +54,6 @@ export const setTlsConfig = (config) => async (dispatch, getState) => {
|
|||||||
|
|
||||||
dispatch(setTlsConfigSuccess(response));
|
dispatch(setTlsConfigSuccess(response));
|
||||||
dispatch(addSuccessToast('encryption_config_saved'));
|
dispatch(addSuccessToast('encryption_config_saved'));
|
||||||
redirectToCurrentProtocol(response, httpPort);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
dispatch(addErrorToast({ error }));
|
dispatch(addErrorToast({ error }));
|
||||||
dispatch(setTlsConfigFailure());
|
dispatch(setTlsConfigFailure());
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const Form = (props) => {
|
|||||||
name={FORM_NAMES.search}
|
name={FORM_NAMES.search}
|
||||||
component={renderFilterField}
|
component={renderFilterField}
|
||||||
type="text"
|
type="text"
|
||||||
className={classNames('form-control--search form-control--transparent', className)}
|
className={classNames('form-control form-control--search form-control--transparent', className)}
|
||||||
placeholder={t('domain_or_client')}
|
placeholder={t('domain_or_client')}
|
||||||
tooltip={t('query_log_strict_search')}
|
tooltip={t('query_log_strict_search')}
|
||||||
onClearInputClick={onInputClear}
|
onClearInputClick={onInputClear}
|
||||||
|
|||||||
@@ -103,14 +103,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form-control--search {
|
.form-control--search {
|
||||||
box-shadow: 0 1px 0 #ddd;
|
|
||||||
padding: 0 2.5rem;
|
padding: 0 2.5rem;
|
||||||
height: 2.25rem;
|
height: 2.25rem;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-control--transparent {
|
.form-control--transparent {
|
||||||
border: 0 solid transparent !important;
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,10 +172,8 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
width: 2.5rem;
|
||||||
--size: 2.5rem;
|
height: 2.5rem;
|
||||||
width: var(--size);
|
|
||||||
height: var(--size);
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin-left: 0.9375rem;
|
margin-left: 0.9375rem;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
@@ -474,7 +470,7 @@
|
|||||||
|
|
||||||
.filteringRules__filter {
|
.filteringRules__filter {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import Select from 'react-select';
|
|||||||
import i18n from '../../../i18n';
|
import i18n from '../../../i18n';
|
||||||
import Tabs from '../../ui/Tabs';
|
import Tabs from '../../ui/Tabs';
|
||||||
import Examples from '../Dns/Upstream/Examples';
|
import Examples from '../Dns/Upstream/Examples';
|
||||||
import { toggleAllServices } from '../../../helpers/helpers';
|
import { toggleAllServices, trimLinesAndRemoveEmpty } from '../../../helpers/helpers';
|
||||||
import {
|
import {
|
||||||
renderInputField,
|
renderInputField,
|
||||||
renderGroupField,
|
renderGroupField,
|
||||||
CheckboxField,
|
CheckboxField,
|
||||||
renderServiceField,
|
renderServiceField,
|
||||||
|
renderTextareaField,
|
||||||
} from '../../../helpers/form';
|
} from '../../../helpers/form';
|
||||||
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
import { validateClientId, validateRequiredValue } from '../../../helpers/validators';
|
||||||
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
|
import { CLIENT_ID_LINK, FORM_NAME } from '../../../helpers/constants';
|
||||||
@@ -230,10 +231,11 @@ let Form = (props) => {
|
|||||||
<Field
|
<Field
|
||||||
id="upstreams"
|
id="upstreams"
|
||||||
name="upstreams"
|
name="upstreams"
|
||||||
component="textarea"
|
component={renderTextareaField}
|
||||||
type="text"
|
type="text"
|
||||||
className="form-control form-control--textarea mb-5"
|
className="form-control form-control--textarea mb-5"
|
||||||
placeholder={t('upstream_dns')}
|
placeholder={t('upstream_dns')}
|
||||||
|
normalizeOnBlur={trimLinesAndRemoveEmpty}
|
||||||
/>
|
/>
|
||||||
<Examples />
|
<Examples />
|
||||||
</div>,
|
</div>,
|
||||||
|
|||||||
@@ -390,6 +390,7 @@ export const SPECIAL_FILTER_ID = {
|
|||||||
PARENTAL: -3,
|
PARENTAL: -3,
|
||||||
SAFE_BROWSING: -4,
|
SAFE_BROWSING: -4,
|
||||||
SAFE_SEARCH: -5,
|
SAFE_SEARCH: -5,
|
||||||
|
REWRITES: -6,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BLOCK_ACTIONS = {
|
export const BLOCK_ACTIONS = {
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export default {
|
|||||||
"homepage": "https://badmojr.github.io/1Hosts/",
|
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_24.txt"
|
||||||
},
|
},
|
||||||
|
"1hosts_mini": {
|
||||||
|
"name": "1Hosts (mini)",
|
||||||
|
"categoryId": "general",
|
||||||
|
"homepage": "https://badmojr.github.io/1Hosts/",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_38.txt"
|
||||||
|
},
|
||||||
"CHN_adrules": {
|
"CHN_adrules": {
|
||||||
"name": "CHN: AdRules DNS List",
|
"name": "CHN: AdRules DNS List",
|
||||||
"categoryId": "regional",
|
"categoryId": "regional",
|
||||||
@@ -40,6 +46,12 @@ export default {
|
|||||||
"homepage": "https://anti-ad.net/",
|
"homepage": "https://anti-ad.net/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_21.txt"
|
||||||
},
|
},
|
||||||
|
"HUN_hufilter": {
|
||||||
|
"name": "HUN: Hufilter",
|
||||||
|
"categoryId": "regional",
|
||||||
|
"homepage": "https://github.com/hufilter/hufilter",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_35.txt"
|
||||||
|
},
|
||||||
"IDN_abpindo": {
|
"IDN_abpindo": {
|
||||||
"name": "IDN: ABPindo",
|
"name": "IDN: ABPindo",
|
||||||
"categoryId": "regional",
|
"categoryId": "regional",
|
||||||
@@ -70,6 +82,12 @@ export default {
|
|||||||
"homepage": "https://github.com/yous/YousList",
|
"homepage": "https://github.com/yous/YousList",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_15.txt"
|
||||||
},
|
},
|
||||||
|
"LIT_easylist_lithuania": {
|
||||||
|
"name": "LIT: EasyList Lithuania",
|
||||||
|
"categoryId": "regional",
|
||||||
|
"homepage": "https://github.com/EasyList-Lithuania/easylist_lithuania",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_36.txt"
|
||||||
|
},
|
||||||
"MKD_macedonian_pi_hole_blocklist": {
|
"MKD_macedonian_pi_hole_blocklist": {
|
||||||
"name": "MKD: Macedonian Pi-hole Blocklist",
|
"name": "MKD: Macedonian Pi-hole Blocklist",
|
||||||
"categoryId": "regional",
|
"categoryId": "regional",
|
||||||
@@ -148,6 +166,18 @@ export default {
|
|||||||
"homepage": "https://energized.pro/",
|
"homepage": "https://energized.pro/",
|
||||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_28.txt"
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_28.txt"
|
||||||
},
|
},
|
||||||
|
"hagezi_personal": {
|
||||||
|
"name": "HaGeZi Personal Black \u0026 White",
|
||||||
|
"categoryId": "general",
|
||||||
|
"homepage": "https://github.com/hagezi/dns-blocklists",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_34.txt"
|
||||||
|
},
|
||||||
|
"no_google": {
|
||||||
|
"name": "No Google",
|
||||||
|
"categoryId": "other",
|
||||||
|
"homepage": "https://github.com/nickspaargaren/no-google",
|
||||||
|
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_37.txt"
|
||||||
|
},
|
||||||
"nocoin_filter_list": {
|
"nocoin_filter_list": {
|
||||||
"name": "NoCoin Filter List",
|
"name": "NoCoin Filter List",
|
||||||
"categoryId": "security",
|
"categoryId": "security",
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
|||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.46.4
|
github.com/AdguardTeam/dnsproxy v0.46.5
|
||||||
github.com/AdguardTeam/golibs v0.11.3
|
github.com/AdguardTeam/golibs v0.11.3
|
||||||
github.com/AdguardTeam/urlfilter v0.16.0
|
github.com/AdguardTeam/urlfilter v0.16.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
@@ -30,8 +30,8 @@ require (
|
|||||||
go.etcd.io/bbolt v1.3.6
|
go.etcd.io/bbolt v1.3.6
|
||||||
golang.org/x/crypto v0.1.0
|
golang.org/x/crypto v0.1.0
|
||||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
|
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
|
||||||
golang.org/x/net v0.1.0
|
golang.org/x/net v0.4.0
|
||||||
golang.org/x/sys v0.2.0
|
golang.org/x/sys v0.3.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
howett.net/plist v1.0.0
|
howett.net/plist v1.0.0
|
||||||
@@ -61,6 +61,6 @@ require (
|
|||||||
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
github.com/u-root/uio v0.0.0-20220204230159-dac05f7d2cb4 // indirect
|
||||||
golang.org/x/mod v0.6.0 // indirect
|
golang.org/x/mod v0.6.0 // indirect
|
||||||
golang.org/x/sync v0.1.0 // indirect
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
golang.org/x/text v0.4.0 // indirect
|
golang.org/x/text v0.5.0 // indirect
|
||||||
golang.org/x/tools v0.2.0 // indirect
|
golang.org/x/tools v0.2.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,5 +1,5 @@
|
|||||||
github.com/AdguardTeam/dnsproxy v0.46.4 h1:/+wnTG0q2TkGQyA1PeSsjv4/f5ZprGduKPSoOcG+rOU=
|
github.com/AdguardTeam/dnsproxy v0.46.5 h1:TiJZhwaIDDaKkqEfJ9AD9aroFjcHN8oEbKB8WfTjSIs=
|
||||||
github.com/AdguardTeam/dnsproxy v0.46.4/go.mod h1:yYDMAH6ay2PxLcLtfVM3FUiyv/U9B/zYO+cIIssuJNU=
|
github.com/AdguardTeam/dnsproxy v0.46.5/go.mod h1:yKBVgFlE6CqTQtye++3e7SATaMPc4Ixij+KkHsM6HhM=
|
||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.11.3 h1:Oif+REq2WLycQ2Xm3ZPmJdfftptss0HbGWbxdFaC310=
|
github.com/AdguardTeam/golibs v0.11.3 h1:Oif+REq2WLycQ2Xm3ZPmJdfftptss0HbGWbxdFaC310=
|
||||||
@@ -187,8 +187,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
|||||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
|
||||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -228,8 +228,8 @@ golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
|
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -237,8 +237,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
|||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package aghtest
|
package aghtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
@@ -116,6 +118,36 @@ func (w *FSWatcher) Close() (err error) {
|
|||||||
return w.OnClose()
|
return w.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package agh
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||||
|
type ServiceWithConfig[ConfigType any] struct {
|
||||||
|
OnStart func() (err error)
|
||||||
|
OnShutdown func(ctx context.Context) (err error)
|
||||||
|
OnConfig func() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||||
|
return s.OnStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||||
|
return s.OnShutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||||
|
return s.OnConfig()
|
||||||
|
}
|
||||||
|
|
||||||
// Module dnsproxy
|
// Module dnsproxy
|
||||||
|
|
||||||
// Package upstream
|
// Package upstream
|
||||||
|
|||||||
@@ -530,14 +530,14 @@ func validateBlockingMode(mode BlockingMode, blockingIPv4, blockingIPv6 net.IP)
|
|||||||
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS
|
// prepareInternalProxy initializes the DNS proxy that is used for internal DNS
|
||||||
// queries, such as public clients PTR resolving and updater hostname resolving.
|
// queries, such as public clients PTR resolving and updater hostname resolving.
|
||||||
func (s *Server) prepareInternalProxy() (err error) {
|
func (s *Server) prepareInternalProxy() (err error) {
|
||||||
|
srvConf := s.conf
|
||||||
conf := &proxy.Config{
|
conf := &proxy.Config{
|
||||||
CacheEnabled: true,
|
CacheEnabled: true,
|
||||||
CacheSizeBytes: 4096,
|
CacheSizeBytes: 4096,
|
||||||
UpstreamConfig: s.conf.UpstreamConfig,
|
UpstreamConfig: srvConf.UpstreamConfig,
|
||||||
MaxGoroutines: int(s.conf.MaxGoroutines),
|
MaxGoroutines: int(s.conf.MaxGoroutines),
|
||||||
}
|
}
|
||||||
|
|
||||||
srvConf := s.conf
|
|
||||||
setProxyUpstreamMode(
|
setProxyUpstreamMode(
|
||||||
conf,
|
conf,
|
||||||
srvConf.AllServers,
|
srvConf.AllServers,
|
||||||
@@ -570,46 +570,32 @@ func (s *Server) Stop() error {
|
|||||||
|
|
||||||
// stopLocked stops the DNS server without locking. For internal use only.
|
// stopLocked stops the DNS server without locking. For internal use only.
|
||||||
func (s *Server) stopLocked() (err error) {
|
func (s *Server) stopLocked() (err error) {
|
||||||
|
// TODO(e.burkov, a.garipov): Return critical errors, not just log them.
|
||||||
|
// This will require filtering all the non-critical errors in
|
||||||
|
// [upstream.Upstream] implementations.
|
||||||
|
|
||||||
if s.dnsProxy != nil {
|
if s.dnsProxy != nil {
|
||||||
err = s.dnsProxy.Stop()
|
err = s.dnsProxy.Stop()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("closing primary resolvers: %w", err)
|
log.Error("dnsforward: closing primary resolvers: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil {
|
if upsConf := s.internalProxy.UpstreamConfig; upsConf != nil {
|
||||||
const action = "closing internal resolvers"
|
|
||||||
|
|
||||||
err = upsConf.Close()
|
err = upsConf.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
log.Error("dnsforward: closing internal resolvers: %s", err)
|
||||||
log.Debug("dnsforward: %s: %s", action, err)
|
|
||||||
} else {
|
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", action, err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil {
|
if upsConf := s.localResolvers.UpstreamConfig; upsConf != nil {
|
||||||
const action = "closing local resolvers"
|
|
||||||
|
|
||||||
err = upsConf.Close()
|
err = upsConf.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, net.ErrClosed) {
|
log.Error("dnsforward: closing local resolvers: %s", err)
|
||||||
log.Debug("dnsforward: %s: %s", action, err)
|
|
||||||
} else {
|
|
||||||
errs = append(errs, fmt.Errorf("%s: %w", action, err))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
s.isRunning = false
|
||||||
return errors.List("stopping dns server", errs...)
|
|
||||||
} else {
|
|
||||||
s.isRunning = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/golibs/netutil"
|
||||||
@@ -67,7 +68,7 @@ func createTestServer(
|
|||||||
ID: 0, Data: []byte(rules),
|
ID: 0, Data: []byte(rules),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
f, err := filtering.New(filterConf, filters)
|
f, err := filtering.New(filterConf, filters, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
f.SetEnabled(true)
|
f.SetEnabled(true)
|
||||||
@@ -760,7 +761,7 @@ func TestBlockedCustomIP(t *testing.T) {
|
|||||||
Data: []byte(rules),
|
Data: []byte(rules),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
f, err := filtering.New(&filtering.Config{}, filters)
|
f, err := filtering.New(&filtering.Config{}, filters, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s, err := NewServer(DNSCreateParams{
|
s, err := NewServer(DNSCreateParams{
|
||||||
@@ -880,21 +881,22 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
|
|||||||
|
|
||||||
func TestRewrite(t *testing.T) {
|
func TestRewrite(t *testing.T) {
|
||||||
c := &filtering.Config{
|
c := &filtering.Config{
|
||||||
Rewrites: []*filtering.LegacyRewrite{{
|
Rewrites: []*filtering.RewriteItem{{
|
||||||
Domain: "test.com",
|
Domain: "test.com",
|
||||||
Answer: "1.2.3.4",
|
Answer: "1.2.3.4",
|
||||||
Type: dns.TypeA,
|
|
||||||
}, {
|
}, {
|
||||||
Domain: "alias.test.com",
|
Domain: "alias.test.com",
|
||||||
Answer: "test.com",
|
Answer: "test.com",
|
||||||
Type: dns.TypeCNAME,
|
|
||||||
}, {
|
}, {
|
||||||
Domain: "my.alias.example.org",
|
Domain: "my.alias.example.org",
|
||||||
Answer: "example.org",
|
Answer: "example.org",
|
||||||
Type: dns.TypeCNAME,
|
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
f, err := filtering.New(c, nil)
|
|
||||||
|
rewriteStorage, err := rewrite.NewDefaultStorage(c.Rewrites)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := filtering.New(c, nil, rewriteStorage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
f.SetEnabled(true)
|
f.SetEnabled(true)
|
||||||
@@ -945,6 +947,12 @@ func TestRewrite(t *testing.T) {
|
|||||||
|
|
||||||
assert.Empty(t, reply.Answer)
|
assert.Empty(t, reply.Answer)
|
||||||
|
|
||||||
|
req = createTestMessageWithType("test.com.", dns.TypeTXT)
|
||||||
|
reply, eerr = dns.Exchange(req, addr.String())
|
||||||
|
require.NoError(t, eerr)
|
||||||
|
|
||||||
|
assert.Empty(t, reply.Answer)
|
||||||
|
|
||||||
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
req = createTestMessageWithType("alias.test.com.", dns.TypeA)
|
||||||
reply, eerr = dns.Exchange(req, addr.String())
|
reply, eerr = dns.Exchange(req, addr.String())
|
||||||
require.NoError(t, eerr)
|
require.NoError(t, eerr)
|
||||||
@@ -952,8 +960,15 @@ func TestRewrite(t *testing.T) {
|
|||||||
require.Len(t, reply.Answer, 2)
|
require.Len(t, reply.Answer, 2)
|
||||||
|
|
||||||
assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
|
assert.Equal(t, "test.com.", reply.Answer[0].(*dns.CNAME).Target)
|
||||||
|
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
|
||||||
assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))
|
assert.True(t, net.IP{1, 2, 3, 4}.Equal(reply.Answer[1].(*dns.A).A))
|
||||||
|
|
||||||
|
req = createTestMessageWithType("alias.test.com.", dns.TypeTXT)
|
||||||
|
reply, eerr = dns.Exchange(req, addr.String())
|
||||||
|
require.NoError(t, eerr)
|
||||||
|
|
||||||
|
assert.Empty(t, reply.Answer)
|
||||||
|
|
||||||
req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
|
req = createTestMessageWithType("my.alias.example.org.", dns.TypeA)
|
||||||
reply, eerr = dns.Exchange(req, addr.String())
|
reply, eerr = dns.Exchange(req, addr.String())
|
||||||
require.NoError(t, eerr)
|
require.NoError(t, eerr)
|
||||||
@@ -967,6 +982,12 @@ func TestRewrite(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
|
assert.Equal(t, "example.org.", reply.Answer[0].(*dns.CNAME).Target)
|
||||||
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
|
assert.Equal(t, dns.TypeA, reply.Answer[1].Header().Rrtype)
|
||||||
|
|
||||||
|
req = createTestMessageWithType("my.alias.test.com.", dns.TypeTXT)
|
||||||
|
reply, eerr = dns.Exchange(req, addr.String())
|
||||||
|
require.NoError(t, eerr)
|
||||||
|
|
||||||
|
assert.Empty(t, reply.Answer)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, protect := range []bool{true, false} {
|
for _, protect := range []bool{true, false} {
|
||||||
@@ -1011,7 +1032,7 @@ var testDHCP = &dhcpd.MockInterface{
|
|||||||
func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||||
const localDomain = "lan"
|
const localDomain = "lan"
|
||||||
|
|
||||||
flt, err := filtering.New(&filtering.Config{}, nil)
|
flt, err := filtering.New(&filtering.Config{}, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s, err := NewServer(DNSCreateParams{
|
s, err := NewServer(DNSCreateParams{
|
||||||
@@ -1085,7 +1106,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
|||||||
|
|
||||||
flt, err := filtering.New(&filtering.Config{
|
flt, err := filtering.New(&filtering.Config{
|
||||||
EtcHosts: hc,
|
EtcHosts: hc,
|
||||||
}, nil)
|
}, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
flt.SetEnabled(true)
|
flt.SetEnabled(true)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
|||||||
ID: 0, Data: []byte(rules),
|
ID: 0, Data: []byte(rules),
|
||||||
}}
|
}}
|
||||||
|
|
||||||
f, err := filtering.New(&filtering.Config{}, filters)
|
f, err := filtering.New(&filtering.Config{}, filters, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
f.SetEnabled(true)
|
f.SetEnabled(true)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package filtering
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
@@ -97,14 +99,15 @@ func (d *DNSFilter) filterSetProperties(
|
|||||||
filt.URL,
|
filt.URL,
|
||||||
)
|
)
|
||||||
|
|
||||||
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time) {
|
defer func(oldURL, oldName string, oldEnabled bool, oldUpdated time.Time, oldRulesCount int) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
filt.URL = oldURL
|
filt.URL = oldURL
|
||||||
filt.Name = oldName
|
filt.Name = oldName
|
||||||
filt.Enabled = oldEnabled
|
filt.Enabled = oldEnabled
|
||||||
filt.LastUpdated = oldUpdated
|
filt.LastUpdated = oldUpdated
|
||||||
|
filt.RulesCount = oldRulesCount
|
||||||
}
|
}
|
||||||
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated)
|
}(filt.URL, filt.Name, filt.Enabled, filt.LastUpdated, filt.RulesCount)
|
||||||
|
|
||||||
filt.Name = newList.Name
|
filt.Name = newList.Name
|
||||||
|
|
||||||
@@ -134,8 +137,8 @@ func (d *DNSFilter) filterSetProperties(
|
|||||||
// TODO(e.burkov): The validation of the contents of the new URL is
|
// TODO(e.burkov): The validation of the contents of the new URL is
|
||||||
// currently skipped if the rule list is disabled. This makes it
|
// currently skipped if the rule list is disabled. This makes it
|
||||||
// possible to set a bad rules source, but the validation should still
|
// possible to set a bad rules source, but the validation should still
|
||||||
// kick in when the filter is enabled. Consider making changing this
|
// kick in when the filter is enabled. Consider changing this behavior
|
||||||
// behavior to be stricter.
|
// to be stricter.
|
||||||
filt.unload()
|
filt.unload()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,10 +272,10 @@ func (d *DNSFilter) periodicallyRefreshFilters() {
|
|||||||
// already going on.
|
// already going on.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
// TODO(e.burkov): Get rid of the concurrency pattern which requires the
|
||||||
// sync.Mutex.TryLock.
|
// [sync.Mutex.TryLock].
|
||||||
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
func (d *DNSFilter) tryRefreshFilters(block, allow, force bool) (updated int, isNetworkErr, ok bool) {
|
||||||
if ok = d.refreshLock.TryLock(); !ok {
|
if ok = d.refreshLock.TryLock(); !ok {
|
||||||
return 0, false, ok
|
return 0, false, false
|
||||||
}
|
}
|
||||||
defer d.refreshLock.Unlock()
|
defer d.refreshLock.Unlock()
|
||||||
|
|
||||||
@@ -427,52 +430,124 @@ func (d *DNSFilter) refreshFiltersIntl(block, allow, force bool) (int, bool) {
|
|||||||
return updNum, false
|
return updNum, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows printable UTF-8 text with CR, LF, TAB characters
|
// isPrintableText returns true if data is printable UTF-8 text with CR, LF, TAB
|
||||||
func isPrintableText(data []byte, len int) bool {
|
// characters.
|
||||||
for i := 0; i < len; i++ {
|
//
|
||||||
c := data[i]
|
// TODO(e.burkov): Investigate the purpose of this and improve the
|
||||||
|
// implementation. Perhaps, use something from the unicode package.
|
||||||
|
func isPrintableText(data string) (ok bool) {
|
||||||
|
for _, c := range []byte(data) {
|
||||||
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
if (c >= ' ' && c != 0x7f) || c == '\n' || c == '\r' || c == '\t' {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
|
// scanLinesWithBreak is essentially a [bufio.ScanLines] which keeps trailing
|
||||||
func (d *DNSFilter) parseFilterContents(file io.Reader) (int, uint32, string) {
|
// line breaks.
|
||||||
rulesCount := 0
|
func scanLinesWithBreak(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
name := ""
|
if atEOF && len(data) == 0 {
|
||||||
seenTitle := false
|
return 0, nil, nil
|
||||||
r := bufio.NewReader(file)
|
}
|
||||||
checksum := uint32(0)
|
|
||||||
|
|
||||||
for {
|
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||||
line, err := r.ReadString('\n')
|
return i + 1, data[0 : i+1], nil
|
||||||
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
}
|
||||||
|
|
||||||
line = strings.TrimSpace(line)
|
if atEOF {
|
||||||
if len(line) == 0 {
|
return len(data), data, nil
|
||||||
//
|
}
|
||||||
} else if line[0] == '!' {
|
|
||||||
m := d.filterTitleRegexp.FindAllStringSubmatch(line, -1)
|
|
||||||
if len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
|
|
||||||
name = m[0][1]
|
|
||||||
seenTitle = true
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if line[0] == '#' {
|
// Request more data.
|
||||||
//
|
return 0, nil, nil
|
||||||
} else {
|
}
|
||||||
rulesCount++
|
|
||||||
|
// parseFilter copies filter's content from src to dst and returns the number of
|
||||||
|
// rules, name, number of bytes written, checksum, and title of the parsed list.
|
||||||
|
// dst must not be nil.
|
||||||
|
func (d *DNSFilter) parseFilter(
|
||||||
|
src io.Reader,
|
||||||
|
dst io.Writer,
|
||||||
|
) (rulesNum, written int, checksum uint32, title string, err error) {
|
||||||
|
scanner := bufio.NewScanner(src)
|
||||||
|
scanner.Split(scanLinesWithBreak)
|
||||||
|
|
||||||
|
titleFound := false
|
||||||
|
for n := 0; scanner.Scan(); written += n {
|
||||||
|
line := scanner.Text()
|
||||||
|
var isRule bool
|
||||||
|
var likelyTitle string
|
||||||
|
isRule, likelyTitle, err = d.parseFilterLine(line, !titleFound, written == 0)
|
||||||
|
if err != nil {
|
||||||
|
return 0, written, 0, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isRule {
|
||||||
|
rulesNum++
|
||||||
|
} else if likelyTitle != "" {
|
||||||
|
title, titleFound = likelyTitle, true
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum = crc32.Update(checksum, crc32.IEEETable, []byte(line))
|
||||||
|
|
||||||
|
n, err = dst.Write([]byte(line))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
return 0, written, 0, "", fmt.Errorf("writing filter line: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return rulesCount, checksum, name
|
if err = scanner.Err(); err != nil {
|
||||||
|
return 0, written, 0, "", fmt.Errorf("scanning filter contents: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rulesNum, written, checksum, title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFilterLine returns true if the passed line is a rule. line is
|
||||||
|
// considered a rule if it's not a comment and contains no title.
|
||||||
|
func (d *DNSFilter) parseFilterLine(
|
||||||
|
line string,
|
||||||
|
lookForTitle bool,
|
||||||
|
testHTML bool,
|
||||||
|
) (isRule bool, title string, err error) {
|
||||||
|
if !isPrintableText(line) {
|
||||||
|
return false, "", errors.Error("filter contains non-printable characters")
|
||||||
|
}
|
||||||
|
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || line[0] == '#' {
|
||||||
|
return false, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if testHTML && isHTML(line) {
|
||||||
|
return false, "", errors.Error("data is HTML, not plain text")
|
||||||
|
}
|
||||||
|
|
||||||
|
if line[0] == '!' && lookForTitle {
|
||||||
|
match := d.filterTitleRegexp.FindStringSubmatch(line)
|
||||||
|
if len(match) > 1 {
|
||||||
|
title = match[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, title, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isHTML returns true if the line contains HTML tags instead of plain text.
|
||||||
|
// line shouldn have no leading space symbols.
|
||||||
|
//
|
||||||
|
// TODO(ameshkov): It actually gives too much false-positives. Perhaps, just
|
||||||
|
// check if trimmed string begins with angle bracket.
|
||||||
|
func isHTML(line string) (ok bool) {
|
||||||
|
line = strings.ToLower(line)
|
||||||
|
|
||||||
|
return strings.HasPrefix(line, "<html") || strings.HasPrefix(line, "<!doctype")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform upgrade on a filter and update LastUpdated value
|
// Perform upgrade on a filter and update LastUpdated value
|
||||||
@@ -485,57 +560,10 @@ func (d *DNSFilter) update(filter *FilterYAML) (bool, error) {
|
|||||||
log.Error("os.Chtimes(): %v", e)
|
log.Error("os.Chtimes(): %v", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return b, err
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) read(reader io.Reader, tmpFile *os.File, filter *FilterYAML) (int, error) {
|
|
||||||
htmlTest := true
|
|
||||||
firstChunk := make([]byte, 4*1024)
|
|
||||||
firstChunkLen := 0
|
|
||||||
buf := make([]byte, 64*1024)
|
|
||||||
total := 0
|
|
||||||
for {
|
|
||||||
n, err := reader.Read(buf)
|
|
||||||
total += n
|
|
||||||
|
|
||||||
if htmlTest {
|
|
||||||
num := len(firstChunk) - firstChunkLen
|
|
||||||
if n < num {
|
|
||||||
num = n
|
|
||||||
}
|
|
||||||
copied := copy(firstChunk[firstChunkLen:], buf[:num])
|
|
||||||
firstChunkLen += copied
|
|
||||||
|
|
||||||
if firstChunkLen == len(firstChunk) || err == io.EOF {
|
|
||||||
if !isPrintableText(firstChunk, firstChunkLen) {
|
|
||||||
return total, fmt.Errorf("data contains non-printable characters")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := strings.ToLower(string(firstChunk))
|
|
||||||
if strings.Contains(s, "<html") || strings.Contains(s, "<!doctype") {
|
|
||||||
return total, fmt.Errorf("data is HTML, not plain text")
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlTest = false
|
|
||||||
firstChunk = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err2 := tmpFile.Write(buf[:n])
|
|
||||||
if err2 != nil {
|
|
||||||
return total, err2
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == io.EOF {
|
|
||||||
return total, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
|
|
||||||
return total, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
// finalizeUpdate closes and gets rid of temporary file f with filter's content
|
||||||
// according to updated. It also saves new values of flt's name, rules number
|
// according to updated. It also saves new values of flt's name, rules number
|
||||||
// and checksum if sucсeeded.
|
// and checksum if sucсeeded.
|
||||||
@@ -552,7 +580,8 @@ func (d *DNSFilter) finalizeUpdate(
|
|||||||
// Close the file before renaming it because it's required on Windows.
|
// Close the file before renaming it because it's required on Windows.
|
||||||
//
|
//
|
||||||
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
// See https://github.com/adguardTeam/adGuardHome/issues/1553.
|
||||||
if err = file.Close(); err != nil {
|
err = file.Close()
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("closing temporary file: %w", err)
|
return fmt.Errorf("closing temporary file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,38 +593,18 @@ func (d *DNSFilter) finalizeUpdate(
|
|||||||
|
|
||||||
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
log.Printf("saving filter %d contents to: %s", flt.ID, flt.Path(d.DataDir))
|
||||||
|
|
||||||
if err = os.Rename(tmpFileName, flt.Path(d.DataDir)); err != nil {
|
// Don't use renamio or maybe packages, since those will require loading the
|
||||||
|
// whole filter content to the memory on Windows.
|
||||||
|
err = os.Rename(tmpFileName, flt.Path(d.DataDir))
|
||||||
|
if err != nil {
|
||||||
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
return errors.WithDeferred(err, os.Remove(tmpFileName))
|
||||||
}
|
}
|
||||||
|
|
||||||
flt.Name = stringutil.Coalesce(flt.Name, name)
|
flt.Name, flt.checksum, flt.RulesCount = aghalg.Coalesce(flt.Name, name), cs, rnum
|
||||||
flt.checksum = cs
|
|
||||||
flt.RulesCount = rnum
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// processUpdate copies filter's content from src to dst and returns the name,
|
|
||||||
// rules number, and checksum for it. It also returns the number of bytes read
|
|
||||||
// from src.
|
|
||||||
func (d *DNSFilter) processUpdate(
|
|
||||||
src io.Reader,
|
|
||||||
dst *os.File,
|
|
||||||
flt *FilterYAML,
|
|
||||||
) (name string, rnum int, cs uint32, n int, err error) {
|
|
||||||
if n, err = d.read(src, dst, flt); err != nil {
|
|
||||||
return "", 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = dst.Seek(0, io.SeekStart); err != nil {
|
|
||||||
return "", 0, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rnum, cs, name = d.parseFilterContents(dst)
|
|
||||||
|
|
||||||
return name, rnum, cs, n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
// updateIntl updates the flt rewriting it's actual file. It returns true if
|
||||||
// the actual update has been performed.
|
// the actual update has been performed.
|
||||||
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
||||||
@@ -612,31 +621,21 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
|||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
err = errors.WithDeferred(err, d.finalizeUpdate(tmpFile, flt, ok, name, rnum, cs))
|
||||||
ok = ok && err == nil
|
if ok && err == nil {
|
||||||
if ok {
|
|
||||||
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
log.Printf("updated filter %d: %d bytes, %d rules", flt.ID, n, rnum)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Change the default 0o600 permission to something more acceptable by
|
// Change the default 0o600 permission to something more acceptable by end
|
||||||
// end users.
|
// users.
|
||||||
//
|
//
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/3198.
|
||||||
if err = tmpFile.Chmod(0o644); err != nil {
|
if err = tmpFile.Chmod(0o644); err != nil {
|
||||||
return false, fmt.Errorf("changing file mode: %w", err)
|
return false, fmt.Errorf("changing file mode: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var r io.Reader
|
var rc io.ReadCloser
|
||||||
if filepath.IsAbs(flt.URL) {
|
if !filepath.IsAbs(flt.URL) {
|
||||||
var file io.ReadCloser
|
|
||||||
file, err = os.Open(flt.URL)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("open file: %w", err)
|
|
||||||
}
|
|
||||||
defer func() { err = errors.WithDeferred(err, file.Close()) }()
|
|
||||||
|
|
||||||
r = file
|
|
||||||
} else {
|
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
resp, err = d.HTTPClient.Get(flt.URL)
|
resp, err = d.HTTPClient.Get(flt.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -649,24 +648,30 @@ func (d *DNSFilter) updateIntl(flt *FilterYAML) (ok bool, err error) {
|
|||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
log.Printf("got status code %d from %s, skip", resp.StatusCode, flt.URL)
|
||||||
|
|
||||||
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
|
return false, fmt.Errorf("got status code %d, want %d", resp.StatusCode, http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
r = resp.Body
|
rc = resp.Body
|
||||||
|
} else {
|
||||||
|
rc, err = os.Open(flt.URL)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { err = errors.WithDeferred(err, rc.Close()) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
name, rnum, cs, n, err = d.processUpdate(r, tmpFile, flt)
|
rnum, n, cs, name, err = d.parseFilter(rc, tmpFile)
|
||||||
|
|
||||||
return cs != flt.checksum, err
|
return cs != flt.checksum && err == nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// loads filter contents from the file in dataDir
|
// loads filter contents from the file in dataDir
|
||||||
func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
func (d *DNSFilter) load(flt *FilterYAML) (err error) {
|
||||||
filterFilePath := filter.Path(d.DataDir)
|
fileName := flt.Path(d.DataDir)
|
||||||
|
|
||||||
log.Tracef("filtering: loading filter %d from %s", filter.ID, filterFilePath)
|
log.Debug("filtering: loading filter %d from %s", flt.ID, fileName)
|
||||||
|
|
||||||
file, err := os.Open(filterFilePath)
|
file, err := os.Open(fileName)
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
// Do nothing, file doesn't exist.
|
// Do nothing, file doesn't exist.
|
||||||
return nil
|
return nil
|
||||||
@@ -680,13 +685,14 @@ func (d *DNSFilter) load(filter *FilterYAML) (err error) {
|
|||||||
return fmt.Errorf("getting filter file stat: %w", err)
|
return fmt.Errorf("getting filter file stat: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Tracef("filtering: File %s, id %d, length %d", filterFilePath, filter.ID, st.Size())
|
log.Debug("filtering: file %s, id %d, length %d", fileName, flt.ID, st.Size())
|
||||||
|
|
||||||
rulesCount, checksum, _ := d.parseFilterContents(file)
|
rulesCount, _, checksum, _, err := d.parseFilter(file, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing filter file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
filter.RulesCount = rulesCount
|
flt.RulesCount, flt.checksum, flt.LastUpdated = rulesCount, checksum, st.ModTime()
|
||||||
filter.checksum = checksum
|
|
||||||
filter.LastUpdated = st.ModTime()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,33 +4,23 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/netip"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
// serveHTTPLocally starts a new HTTP server, that handles its index with h. It
|
||||||
// respond with fltContent. It also gracefully closes the listener when the
|
// also gracefully closes the listener when the test under t finishes.
|
||||||
// test under t finishes.
|
func serveHTTPLocally(t *testing.T, h http.Handler) (urlStr string) {
|
||||||
func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
h := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
pt := testutil.PanicT{}
|
|
||||||
|
|
||||||
n, werr := w.Write(fltContent)
|
|
||||||
require.NoError(pt, werr)
|
|
||||||
require.Equal(pt, len(fltContent), n)
|
|
||||||
})
|
|
||||||
|
|
||||||
l, err := net.Listen("tcp", ":0")
|
l, err := net.Listen("tcp", ":0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -38,9 +28,26 @@ func serveFiltersLocally(t *testing.T, fltContent []byte) (ipp netip.AddrPort) {
|
|||||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||||
|
|
||||||
addr := l.Addr()
|
addr := l.Addr()
|
||||||
require.IsType(t, new(net.TCPAddr), addr)
|
require.IsType(t, (*net.TCPAddr)(nil), addr)
|
||||||
|
|
||||||
return netip.AddrPortFrom(netutil.IPv4Localhost(), uint16(addr.(*net.TCPAddr).Port))
|
return (&url.URL{
|
||||||
|
Scheme: aghhttp.SchemeHTTP,
|
||||||
|
Host: addr.String(),
|
||||||
|
}).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveFiltersLocally is a helper that concurrently listens on a free port to
|
||||||
|
// respond with fltContent.
|
||||||
|
func serveFiltersLocally(t *testing.T, fltContent []byte) (urlStr string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
return serveHTTPLocally(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
pt := testutil.PanicT{}
|
||||||
|
|
||||||
|
n, werr := w.Write(fltContent)
|
||||||
|
require.NoError(pt, werr)
|
||||||
|
require.Equal(pt, len(fltContent), n)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilters(t *testing.T) {
|
func TestFilters(t *testing.T) {
|
||||||
@@ -61,14 +68,11 @@ func TestFilters(t *testing.T) {
|
|||||||
HTTPClient: &http.Client{
|
HTTPClient: &http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
f := &FilterYAML{
|
f := &FilterYAML{
|
||||||
URL: (&url.URL{
|
URL: addr,
|
||||||
Scheme: "http",
|
|
||||||
Host: addr.String(),
|
|
||||||
}).String(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
updateAndAssert := func(t *testing.T, want require.BoolAssertionFunc, wantRulesCount int) {
|
||||||
@@ -103,11 +107,7 @@ func TestFilters(t *testing.T) {
|
|||||||
anotherContent := []byte(`||example.com^`)
|
anotherContent := []byte(`||example.com^`)
|
||||||
oldURL := f.URL
|
oldURL := f.URL
|
||||||
|
|
||||||
ipp := serveFiltersLocally(t, anotherContent)
|
f.URL = serveFiltersLocally(t, anotherContent)
|
||||||
f.URL = (&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: ipp.String(),
|
|
||||||
}).String()
|
|
||||||
t.Cleanup(func() { f.URL = oldURL })
|
t.Cleanup(func() { f.URL = oldURL })
|
||||||
|
|
||||||
updateAndAssert(t, require.True, 1)
|
updateAndAssert(t, require.True, 1)
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import (
|
|||||||
// The IDs of built-in filter lists.
|
// The IDs of built-in filter lists.
|
||||||
//
|
//
|
||||||
// Keep in sync with client/src/helpers/constants.js.
|
// Keep in sync with client/src/helpers/constants.js.
|
||||||
// TODO(d.kolyshev): Add RewritesListID and don't forget to keep in sync.
|
|
||||||
const (
|
const (
|
||||||
CustomListID = -iota
|
CustomListID = -iota
|
||||||
SysHostsListID
|
SysHostsListID
|
||||||
@@ -41,6 +40,7 @@ const (
|
|||||||
ParentalListID
|
ParentalListID
|
||||||
SafeBrowsingListID
|
SafeBrowsingListID
|
||||||
SafeSearchListID
|
SafeSearchListID
|
||||||
|
RewritesListID
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceEntry - blocked service array element
|
// ServiceEntry - blocked service array element
|
||||||
@@ -90,7 +90,7 @@ type Config struct {
|
|||||||
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
|
ParentalCacheSize uint `yaml:"parental_cache_size"` // (in bytes)
|
||||||
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
|
CacheTime uint `yaml:"cache_time"` // Element's TTL (in minutes)
|
||||||
|
|
||||||
Rewrites []*LegacyRewrite `yaml:"rewrites"`
|
Rewrites []*RewriteItem `yaml:"rewrites"`
|
||||||
|
|
||||||
// Names of services to block (globally).
|
// Names of services to block (globally).
|
||||||
// Per-client settings can override this configuration.
|
// Per-client settings can override this configuration.
|
||||||
@@ -190,8 +190,12 @@ type DNSFilter struct {
|
|||||||
|
|
||||||
// filterTitleRegexp is the regular expression to retrieve a name of a
|
// filterTitleRegexp is the regular expression to retrieve a name of a
|
||||||
// filter list.
|
// filter list.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Don't use regexp for such a simple text processing task.
|
||||||
filterTitleRegexp *regexp.Regexp
|
filterTitleRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
rewriteStorage RewriteStorage
|
||||||
|
|
||||||
hostCheckers []hostChecker
|
hostCheckers []hostChecker
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +317,7 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
|
|||||||
defer d.confLock.Unlock()
|
defer d.confLock.Unlock()
|
||||||
|
|
||||||
*c = d.Config
|
*c = d.Config
|
||||||
c.Rewrites = cloneRewrites(c.Rewrites)
|
c.Rewrites = slices.Clone(c.Rewrites)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
d.filtersMu.RLock()
|
d.filtersMu.RLock()
|
||||||
@@ -324,16 +328,6 @@ func (d *DNSFilter) WriteDiskConfig(c *Config) {
|
|||||||
c.UserRules = slices.Clone(d.UserRules)
|
c.UserRules = slices.Clone(d.UserRules)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cloneRewrites returns a deep copy of entries.
|
|
||||||
func cloneRewrites(entries []*LegacyRewrite) (clone []*LegacyRewrite) {
|
|
||||||
clone = make([]*LegacyRewrite, len(entries))
|
|
||||||
for i, rw := range entries {
|
|
||||||
clone[i] = rw.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFilters sets new filters, synchronously or asynchronously. When filters
|
// SetFilters sets new filters, synchronously or asynchronously. When filters
|
||||||
// are set asynchronously, the old filters continue working until the new
|
// are set asynchronously, the old filters continue working until the new
|
||||||
// filters are ready.
|
// filters are ready.
|
||||||
@@ -544,75 +538,52 @@ func (d *DNSFilter) matchSysHosts(
|
|||||||
// CNAME, breaking loops in the process.
|
// CNAME, breaking loops in the process.
|
||||||
//
|
//
|
||||||
// Secondly, it finds A or AAAA rewrites for host and, if found, sets res.IPList
|
// Secondly, it finds A or AAAA rewrites for host and, if found, sets res.IPList
|
||||||
// accordingly. If the found rewrite has a special value of "A" or "AAAA", the
|
// accordingly.
|
||||||
// result is an exception.
|
|
||||||
func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
|
func (d *DNSFilter) processRewrites(host string, qtype uint16) (res Result) {
|
||||||
d.confLock.RLock()
|
d.confLock.RLock()
|
||||||
defer d.confLock.RUnlock()
|
defer d.confLock.RUnlock()
|
||||||
|
|
||||||
rewrites, matched := findRewrites(d.Rewrites, host, qtype)
|
if d.rewriteStorage == nil {
|
||||||
if !matched {
|
return res
|
||||||
return Result{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Reason = Rewritten
|
dnsr := d.rewriteStorage.MatchRequest(&urlfilter.DNSRequest{
|
||||||
|
Hostname: host,
|
||||||
|
DNSType: qtype,
|
||||||
|
})
|
||||||
|
|
||||||
cnames := stringutil.NewSet()
|
setRewriteResult(&res, host, dnsr, qtype)
|
||||||
origHost := host
|
|
||||||
for matched && len(rewrites) > 0 && rewrites[0].Type == dns.TypeCNAME {
|
|
||||||
rw := rewrites[0]
|
|
||||||
rwPat := rw.Domain
|
|
||||||
rwAns := rw.Answer
|
|
||||||
|
|
||||||
log.Debug("rewrite: cname for %s is %s", host, rwAns)
|
|
||||||
|
|
||||||
if origHost == rwAns || rwPat == rwAns {
|
|
||||||
// Either a request for the hostname itself or a rewrite of
|
|
||||||
// a pattern onto itself, both of which are an exception rules.
|
|
||||||
// Return a not filtered result.
|
|
||||||
return Result{}
|
|
||||||
} else if host == rwAns && isWildcard(rwPat) {
|
|
||||||
// An "*.example.com → sub.example.com" rewrite matching in a loop.
|
|
||||||
//
|
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
|
|
||||||
|
|
||||||
res.CanonName = host
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
host = rwAns
|
|
||||||
if cnames.Has(host) {
|
|
||||||
log.Info("rewrite: cname loop for %q on %q", origHost, host)
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
cnames.Add(host)
|
|
||||||
res.CanonName = host
|
|
||||||
rewrites, matched = findRewrites(d.Rewrites, host, qtype)
|
|
||||||
}
|
|
||||||
|
|
||||||
setRewriteResult(&res, host, rewrites, qtype)
|
|
||||||
|
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
// setRewriteResult sets the Reason or IPList of res if necessary. res must not
|
// setRewriteResult sets the Reason or IPList of res if necessary. res must not
|
||||||
// be nil.
|
// be nil.
|
||||||
func setRewriteResult(res *Result, host string, rewrites []*LegacyRewrite, qtype uint16) {
|
func setRewriteResult(res *Result, host string, dnsr []*rules.DNSRewrite, qtype uint16) {
|
||||||
for _, rw := range rewrites {
|
if len(dnsr) == 0 {
|
||||||
if rw.Type == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
|
res.Reason = NotFilteredNotFound
|
||||||
if rw.IP == nil {
|
|
||||||
// "A"/"AAAA" exception: allow getting from upstream.
|
|
||||||
res.Reason = NotFilteredNotFound
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Reason = Rewritten
|
||||||
|
|
||||||
|
for _, dnsRewrite := range dnsr {
|
||||||
|
if dnsRewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
|
||||||
|
ip, ok := dnsRewrite.Value.(net.IP)
|
||||||
|
if !ok || ip == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.IPList = append(res.IPList, rw.IP)
|
if qtype == dns.TypeA {
|
||||||
|
ip = ip.To4()
|
||||||
|
}
|
||||||
|
|
||||||
log.Debug("rewrite: a/aaaa for %s is %s", host, rw.IP)
|
res.IPList = append(res.IPList, ip)
|
||||||
|
|
||||||
|
log.Debug("rewrite: a/aaaa for %s is %s", host, ip)
|
||||||
|
} else if dnsRewrite.NewCNAME != "" {
|
||||||
|
res.CanonName = dnsRewrite.NewCNAME
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -925,7 +896,7 @@ func InitModule() {
|
|||||||
|
|
||||||
// New creates properly initialized DNS Filter that is ready to be used. c must
|
// New creates properly initialized DNS Filter that is ready to be used. c must
|
||||||
// be non-nil.
|
// be non-nil.
|
||||||
func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
func New(c *Config, blockFilters []Filter, rewriteStorage RewriteStorage) (d *DNSFilter, err error) {
|
||||||
d = &DNSFilter{
|
d = &DNSFilter{
|
||||||
resolver: net.DefaultResolver,
|
resolver: net.DefaultResolver,
|
||||||
refreshLock: &sync.Mutex{},
|
refreshLock: &sync.Mutex{},
|
||||||
@@ -978,11 +949,7 @@ func New(c *Config, blockFilters []Filter) (d *DNSFilter, err error) {
|
|||||||
|
|
||||||
d.Config = *c
|
d.Config = *c
|
||||||
d.filtersMu = &sync.RWMutex{}
|
d.filtersMu = &sync.RWMutex{}
|
||||||
|
d.rewriteStorage = rewriteStorage
|
||||||
err = d.prepareRewrites()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rewrites: preparing: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
bsvcs := []string{}
|
bsvcs := []string{}
|
||||||
for _, s := range d.BlockedServices {
|
for _, s := range d.BlockedServices {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
|
|||||||
ProtectionEnabled: true,
|
ProtectionEnabled: true,
|
||||||
FilteringEnabled: true,
|
FilteringEnabled: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if c != nil {
|
if c != nil {
|
||||||
c.SafeBrowsingCacheSize = 10000
|
c.SafeBrowsingCacheSize = 10000
|
||||||
c.ParentalCacheSize = 10000
|
c.ParentalCacheSize = 10000
|
||||||
@@ -58,7 +59,8 @@ func newForTest(t testing.TB, c *Config, filters []Filter) (f *DNSFilter, setts
|
|||||||
// It must not be nil.
|
// It must not be nil.
|
||||||
c = &Config{}
|
c = &Config{}
|
||||||
}
|
}
|
||||||
f, err := New(c, filters)
|
|
||||||
|
f, err := New(c, filters, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
purgeCaches(f)
|
purgeCaches(f)
|
||||||
@@ -417,274 +419,275 @@ func TestMatching(t *testing.T) {
|
|||||||
host string
|
host string
|
||||||
wantReason Reason
|
wantReason Reason
|
||||||
wantIsFiltered bool
|
wantIsFiltered bool
|
||||||
wantDNSType uint16
|
qtype uint16
|
||||||
}{{
|
}{{
|
||||||
name: "sanity",
|
name: "sanity",
|
||||||
rules: "||doubleclick.net^",
|
rules: "||doubleclick.net^",
|
||||||
host: "www.doubleclick.net",
|
host: "www.doubleclick.net",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "sanity",
|
name: "sanity",
|
||||||
rules: "||doubleclick.net^",
|
rules: "||doubleclick.net^",
|
||||||
host: "nodoubleclick.net",
|
host: "nodoubleclick.net",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "sanity",
|
name: "sanity",
|
||||||
rules: "||doubleclick.net^",
|
rules: "||doubleclick.net^",
|
||||||
host: "doubleclick.net.ru",
|
host: "doubleclick.net.ru",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "sanity",
|
name: "sanity",
|
||||||
rules: "||doubleclick.net^",
|
rules: "||doubleclick.net^",
|
||||||
host: sbBlocked,
|
host: sbBlocked,
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "blocking",
|
name: "blocking",
|
||||||
rules: blockingRules,
|
rules: blockingRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "blocking",
|
name: "blocking",
|
||||||
rules: blockingRules,
|
rules: blockingRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "blocking",
|
name: "blocking",
|
||||||
rules: blockingRules,
|
rules: blockingRules,
|
||||||
host: "test.test.example.org",
|
host: "test.test.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "blocking",
|
name: "blocking",
|
||||||
rules: blockingRules,
|
rules: blockingRules,
|
||||||
host: "testexample.org",
|
host: "testexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "blocking",
|
name: "blocking",
|
||||||
rules: blockingRules,
|
rules: blockingRules,
|
||||||
host: "onemoreexample.org",
|
host: "onemoreexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "allowlist",
|
name: "allowlist",
|
||||||
rules: allowlistRules,
|
rules: allowlistRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "allowlist",
|
name: "allowlist",
|
||||||
rules: allowlistRules,
|
rules: allowlistRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "allowlist",
|
name: "allowlist",
|
||||||
rules: allowlistRules,
|
rules: allowlistRules,
|
||||||
host: "test.test.example.org",
|
host: "test.test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "allowlist",
|
name: "allowlist",
|
||||||
rules: allowlistRules,
|
rules: allowlistRules,
|
||||||
host: "testexample.org",
|
host: "testexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "allowlist",
|
name: "allowlist",
|
||||||
rules: allowlistRules,
|
rules: allowlistRules,
|
||||||
host: "onemoreexample.org",
|
host: "onemoreexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "important",
|
name: "important",
|
||||||
rules: importantRules,
|
rules: importantRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "important",
|
name: "important",
|
||||||
rules: importantRules,
|
rules: importantRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "important",
|
name: "important",
|
||||||
rules: importantRules,
|
rules: importantRules,
|
||||||
host: "test.test.example.org",
|
host: "test.test.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "important",
|
name: "important",
|
||||||
rules: importantRules,
|
rules: importantRules,
|
||||||
host: "testexample.org",
|
host: "testexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "important",
|
name: "important",
|
||||||
rules: importantRules,
|
rules: importantRules,
|
||||||
host: "onemoreexample.org",
|
host: "onemoreexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "regex",
|
name: "regex",
|
||||||
rules: regexRules,
|
rules: regexRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "regex",
|
name: "regex",
|
||||||
rules: regexRules,
|
rules: regexRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "regex",
|
name: "regex",
|
||||||
rules: regexRules,
|
rules: regexRules,
|
||||||
host: "test.test.example.org",
|
host: "test.test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "regex",
|
name: "regex",
|
||||||
rules: regexRules,
|
rules: regexRules,
|
||||||
host: "testexample.org",
|
host: "testexample.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "regex",
|
name: "regex",
|
||||||
rules: regexRules,
|
rules: regexRules,
|
||||||
host: "onemoreexample.org",
|
host: "onemoreexample.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "test2.example.org",
|
host: "test2.example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "example.com",
|
host: "example.com",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "exampleeee.com",
|
host: "exampleeee.com",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "onemoreexamsite.com",
|
host: "onemoreexamsite.com",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "testexample.org",
|
host: "testexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "mask",
|
name: "mask",
|
||||||
rules: maskRules,
|
rules: maskRules,
|
||||||
host: "example.co.uk",
|
host: "example.co.uk",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "dnstype",
|
name: "dnstype",
|
||||||
rules: dnstypeRules,
|
rules: dnstypeRules,
|
||||||
host: "onemoreexample.org",
|
host: "onemoreexample.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "dnstype",
|
name: "dnstype",
|
||||||
rules: dnstypeRules,
|
rules: dnstypeRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredNotFound,
|
wantReason: NotFilteredNotFound,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "dnstype",
|
name: "dnstype",
|
||||||
rules: dnstypeRules,
|
rules: dnstypeRules,
|
||||||
host: "example.org",
|
host: "example.org",
|
||||||
wantIsFiltered: true,
|
wantIsFiltered: true,
|
||||||
wantReason: FilteredBlockList,
|
wantReason: FilteredBlockList,
|
||||||
wantDNSType: dns.TypeAAAA,
|
qtype: dns.TypeAAAA,
|
||||||
}, {
|
}, {
|
||||||
name: "dnstype",
|
name: "dnstype",
|
||||||
rules: dnstypeRules,
|
rules: dnstypeRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeA,
|
qtype: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "dnstype",
|
name: "dnstype",
|
||||||
rules: dnstypeRules,
|
rules: dnstypeRules,
|
||||||
host: "test.example.org",
|
host: "test.example.org",
|
||||||
wantIsFiltered: false,
|
wantIsFiltered: false,
|
||||||
wantReason: NotFilteredAllowList,
|
wantReason: NotFilteredAllowList,
|
||||||
wantDNSType: dns.TypeAAAA,
|
qtype: dns.TypeAAAA,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%s-%s", tc.name, tc.host), func(t *testing.T) {
|
||||||
filters := []Filter{{ID: 0, Data: []byte(tc.rules)}}
|
filters := []Filter{{ID: 0, Data: []byte(tc.rules)}}
|
||||||
d, setts := newForTest(t, nil, filters)
|
d, setts := newForTest(t, nil, filters)
|
||||||
t.Cleanup(d.Close)
|
t.Cleanup(d.Close)
|
||||||
|
|
||||||
res, err := d.CheckHost(tc.host, tc.wantDNSType, setts)
|
res, err := d.CheckHost(tc.host, tc.qtype, setts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered)
|
assert.Equalf(t, tc.wantIsFiltered, res.IsFiltered, "Hostname %s has wrong result (%v must be %v)", tc.host, res.IsFiltered, tc.wantIsFiltered)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -30,11 +29,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
|
|||||||
endpoint: &badRulesEndpoint,
|
endpoint: &badRulesEndpoint,
|
||||||
content: []byte(`<html></html>`),
|
content: []byte(`<html></html>`),
|
||||||
}} {
|
}} {
|
||||||
ipp := serveFiltersLocally(t, rulesSource.content)
|
*rulesSource.endpoint = serveFiltersLocally(t, rulesSource.content)
|
||||||
*rulesSource.endpoint = (&url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: ipp.String(),
|
|
||||||
}).String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -110,7 +105,7 @@ func TestDNSFilter_handleFilteringSetURL(t *testing.T) {
|
|||||||
},
|
},
|
||||||
ConfigModified: func() { confModifiedCalled = true },
|
ConfigModified: func() { confModifiedCalled = true },
|
||||||
DataDir: filtersDir,
|
DataDir: filtersDir,
|
||||||
}, nil)
|
}, nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(d.Close)
|
t.Cleanup(d.Close)
|
||||||
|
|
||||||
|
|||||||
42
internal/filtering/rewrite.go
Normal file
42
internal/filtering/rewrite.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,9 +3,11 @@ package rewrite
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
@@ -15,21 +17,6 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Storage is a storage for rewrite rules.
|
|
||||||
type Storage interface {
|
|
||||||
// MatchRequest returns matching dnsrewrites for the specified request.
|
|
||||||
MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite)
|
|
||||||
|
|
||||||
// Add adds item to the storage.
|
|
||||||
Add(item *Item) (err error)
|
|
||||||
|
|
||||||
// Remove deletes item from the storage.
|
|
||||||
Remove(item *Item) (err error)
|
|
||||||
|
|
||||||
// List returns all items from the storage.
|
|
||||||
List() (items []*Item)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultStorage is the default storage for rewrite rules.
|
// DefaultStorage is the default storage for rewrite rules.
|
||||||
type DefaultStorage struct {
|
type DefaultStorage struct {
|
||||||
// mu protects items.
|
// mu protects items.
|
||||||
@@ -42,7 +29,7 @@ type DefaultStorage struct {
|
|||||||
ruleList filterlist.RuleList
|
ruleList filterlist.RuleList
|
||||||
|
|
||||||
// rewrites stores the rewrite entries from configuration.
|
// rewrites stores the rewrite entries from configuration.
|
||||||
rewrites []*Item
|
rewrites []*filtering.RewriteItem
|
||||||
|
|
||||||
// urlFilterID is the synthetic integer identifier for the urlfilter engine.
|
// urlFilterID is the synthetic integer identifier for the urlfilter engine.
|
||||||
//
|
//
|
||||||
@@ -53,16 +40,13 @@ type DefaultStorage struct {
|
|||||||
|
|
||||||
// NewDefaultStorage returns new rewrites storage. listID is used as an
|
// NewDefaultStorage returns new rewrites storage. listID is used as an
|
||||||
// identifier of the underlying rules list. rewrites must not be nil.
|
// identifier of the underlying rules list. rewrites must not be nil.
|
||||||
func NewDefaultStorage(listID int, rewrites []*Item) (s *DefaultStorage, err error) {
|
func NewDefaultStorage(rewrites []*filtering.RewriteItem) (s *DefaultStorage, err error) {
|
||||||
s = &DefaultStorage{
|
s = &DefaultStorage{
|
||||||
mu: &sync.RWMutex{},
|
mu: &sync.RWMutex{},
|
||||||
urlFilterID: listID,
|
urlFilterID: filtering.RewritesListID,
|
||||||
rewrites: rewrites,
|
rewrites: rewrites,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
err = s.resetRules()
|
err = s.resetRules()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -72,9 +56,9 @@ func NewDefaultStorage(listID int, rewrites []*Item) (s *DefaultStorage, err err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ Storage = (*DefaultStorage)(nil)
|
var _ filtering.RewriteStorage = (*DefaultStorage)(nil)
|
||||||
|
|
||||||
// MatchRequest implements the [Storage] interface for *DefaultStorage.
|
// MatchRequest implements the [RewriteStorage] interface for *DefaultStorage.
|
||||||
func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite) {
|
func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.DNSRewrite) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
@@ -84,28 +68,32 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): Check cnames for cycles on initialisation.
|
// TODO(a.garipov): Check cnames for cycles on initialization.
|
||||||
cnames := stringutil.NewSet()
|
cnames := stringutil.NewSet()
|
||||||
host := dReq.Hostname
|
host := dReq.Hostname
|
||||||
|
var lastCNAMERule *rules.NetworkRule
|
||||||
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
|
for len(rrules) > 0 && rrules[0].DNSRewrite != nil && rrules[0].DNSRewrite.NewCNAME != "" {
|
||||||
rule := rrules[0]
|
lastCNAMERule = rrules[0]
|
||||||
rwAns := rule.DNSRewrite.NewCNAME
|
lastDNSRewrite := lastCNAMERule.DNSRewrite
|
||||||
|
rwAns := lastDNSRewrite.NewCNAME
|
||||||
|
|
||||||
log.Debug("rewrite: cname for %s is %s", host, rwAns)
|
log.Debug("rewrite: cname for %s is %s", host, rwAns)
|
||||||
|
|
||||||
if dReq.Hostname == rwAns {
|
if dReq.Hostname == rwAns {
|
||||||
// A request for the hostname itself is an exception rule.
|
// A request for the hostname itself.
|
||||||
// TODO(d.kolyshev): Check rewrite of a pattern onto itself.
|
// TODO(d.kolyshev): Check rewrite of a pattern onto itself.
|
||||||
|
log.Debug("rewrite: request for hostname itself for %q", dReq.Hostname)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if host == rwAns && isWildcard(rule.RuleText) {
|
if host == rwAns && isWildcard(lastCNAMERule.RuleText) {
|
||||||
// An "*.example.com → sub.example.com" rewrite matching in a loop.
|
// An "*.example.com → sub.example.com" rewrite matching in a loop.
|
||||||
//
|
//
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/4016.
|
||||||
|
log.Debug("rewrite: cname wildcard loop for %q on %q", dReq.Hostname, rwAns)
|
||||||
|
|
||||||
return []*rules.DNSRewrite{rule.DNSRewrite}
|
return []*rules.DNSRewrite{lastDNSRewrite}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cnames.Has(rwAns) {
|
if cnames.Has(rwAns) {
|
||||||
@@ -120,21 +108,28 @@ func (s *DefaultStorage) MatchRequest(dReq *urlfilter.DNSRequest) (rws []*rules.
|
|||||||
Hostname: rwAns,
|
Hostname: rwAns,
|
||||||
DNSType: dReq.DNSType,
|
DNSType: dReq.DNSType,
|
||||||
})
|
})
|
||||||
if drules != nil {
|
|
||||||
rrules = drules
|
if drules == nil {
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rrules = drules
|
||||||
host = rwAns
|
host = rwAns
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.collectDNSRewrites(rrules, dReq.DNSType)
|
return s.collectDNSRewrites(rrules, lastCNAMERule, dReq.DNSType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// collectDNSRewrites filters DNSRewrite by question type.
|
// collectDNSRewrites filters DNSRewrite by question type.
|
||||||
func (s *DefaultStorage) collectDNSRewrites(
|
func (s *DefaultStorage) collectDNSRewrites(
|
||||||
rewrites []*rules.NetworkRule,
|
rewrites []*rules.NetworkRule,
|
||||||
|
cnameRule *rules.NetworkRule,
|
||||||
qtyp uint16,
|
qtyp uint16,
|
||||||
) (rws []*rules.DNSRewrite) {
|
) (rws []*rules.DNSRewrite) {
|
||||||
|
if cnameRule != nil {
|
||||||
|
rewrites = append([]*rules.NetworkRule{cnameRule}, rewrites...)
|
||||||
|
}
|
||||||
|
|
||||||
for _, rewrite := range rewrites {
|
for _, rewrite := range rewrites {
|
||||||
dnsRewrite := rewrite.DNSRewrite
|
dnsRewrite := rewrite.DNSRewrite
|
||||||
if matchesQType(dnsRewrite, qtyp) {
|
if matchesQType(dnsRewrite, qtyp) {
|
||||||
@@ -152,8 +147,8 @@ func (s *DefaultStorage) rewriteRulesForReq(dReq *urlfilter.DNSRequest) (rules [
|
|||||||
return res.DNSRewrites()
|
return res.DNSRewrites()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add implements the [Storage] interface for *DefaultStorage.
|
// Add implements the [RewriteStorage] interface for *DefaultStorage.
|
||||||
func (s *DefaultStorage) Add(item *Item) (err error) {
|
func (s *DefaultStorage) Add(item *filtering.RewriteItem) (err error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
@@ -163,16 +158,16 @@ func (s *DefaultStorage) Add(item *Item) (err error) {
|
|||||||
return s.resetRules()
|
return s.resetRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove implements the [Storage] interface for *DefaultStorage.
|
// Remove implements the [RewriteStorage] interface for *DefaultStorage.
|
||||||
func (s *DefaultStorage) Remove(item *Item) (err error) {
|
func (s *DefaultStorage) Remove(item *filtering.RewriteItem) (err error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
arr := []*Item{}
|
arr := []*filtering.RewriteItem{}
|
||||||
|
|
||||||
// TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete?
|
// TODO(d.kolyshev): Use slices.IndexFunc + slices.Delete?
|
||||||
for _, ent := range s.rewrites {
|
for _, ent := range s.rewrites {
|
||||||
if ent.equal(item) {
|
if ent.Equal(item) {
|
||||||
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
|
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@@ -185,8 +180,8 @@ func (s *DefaultStorage) Remove(item *Item) (err error) {
|
|||||||
return s.resetRules()
|
return s.resetRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
// List implements the [Storage] interface for *DefaultStorage.
|
// List implements the [RewriteStorage] interface for *DefaultStorage.
|
||||||
func (s *DefaultStorage) List() (items []*Item) {
|
func (s *DefaultStorage) List() (items []*filtering.RewriteItem) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
|
|
||||||
@@ -198,7 +193,7 @@ func (s *DefaultStorage) resetRules() (err error) {
|
|||||||
// TODO(a.garipov): Use strings.Builder.
|
// TODO(a.garipov): Use strings.Builder.
|
||||||
var rulesText []string
|
var rulesText []string
|
||||||
for _, rewrite := range s.rewrites {
|
for _, rewrite := range s.rewrites {
|
||||||
rulesText = append(rulesText, rewrite.toRule())
|
rulesText = append(rulesText, toRule(rewrite))
|
||||||
}
|
}
|
||||||
|
|
||||||
strList := &filterlist.StringRuleList{
|
strList := &filterlist.StringRuleList{
|
||||||
@@ -222,20 +217,60 @@ func (s *DefaultStorage) resetRules() (err error) {
|
|||||||
|
|
||||||
// matchesQType returns true if dnsrewrite matches the question type qt.
|
// matchesQType returns true if dnsrewrite matches the question type qt.
|
||||||
func matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) {
|
func matchesQType(dnsrr *rules.DNSRewrite, qt uint16) (ok bool) {
|
||||||
// Add CNAMEs, since they match for all types requests.
|
switch qt {
|
||||||
if dnsrr.RRType == dns.TypeCNAME {
|
case dns.TypeA:
|
||||||
|
return dnsrr.RRType != dns.TypeAAAA
|
||||||
|
case dns.TypeAAAA:
|
||||||
|
return dnsrr.RRType != dns.TypeA
|
||||||
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reject types other than A and AAAA.
|
|
||||||
if qt != dns.TypeA && qt != dns.TypeAAAA {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return dnsrr.RRType == qt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isWildcard returns true if pat is a wildcard domain pattern.
|
// isWildcard returns true if pat is a wildcard domain pattern.
|
||||||
func isWildcard(pat string) (res bool) {
|
func isWildcard(pat string) (res bool) {
|
||||||
return strings.HasPrefix(pat, "|*.")
|
return strings.HasPrefix(pat, "|*.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// toRule converts rw to a filter rule.
|
||||||
|
func toRule(rw *filtering.RewriteItem) (res string) {
|
||||||
|
if rw == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
domain := strings.ToLower(rw.Domain)
|
||||||
|
|
||||||
|
dType, exception := rewriteParams(rw)
|
||||||
|
dTypeKey := dns.TypeToString[dType]
|
||||||
|
if exception {
|
||||||
|
return fmt.Sprintf("@@||%s^$dnstype=%s,dnsrewrite", domain, dTypeKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("|%s^$dnsrewrite=NOERROR;%s;%s", domain, dTypeKey, rw.Answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RewriteParams returns dns request type and exception flag for rw.
|
||||||
|
func rewriteParams(rw *filtering.RewriteItem) (dType uint16, exception bool) {
|
||||||
|
switch rw.Answer {
|
||||||
|
case "AAAA":
|
||||||
|
return dns.TypeAAAA, true
|
||||||
|
case "A":
|
||||||
|
return dns.TypeA, true
|
||||||
|
default:
|
||||||
|
// Go on.
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := netip.ParseAddr(rw.Answer)
|
||||||
|
if err != nil {
|
||||||
|
// TODO(d.kolyshev): Validate rw.Answer as a domain name.
|
||||||
|
return dns.TypeCNAME, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Is4() {
|
||||||
|
dType = dns.TypeA
|
||||||
|
} else {
|
||||||
|
dType = dns.TypeAAAA
|
||||||
|
}
|
||||||
|
|
||||||
|
return dType, false
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
"github.com/AdguardTeam/urlfilter/rules"
|
"github.com/AdguardTeam/urlfilter/rules"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
@@ -12,32 +13,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewDefaultStorage(t *testing.T) {
|
func TestNewDefaultStorage(t *testing.T) {
|
||||||
items := []*Item{{
|
items := []*filtering.RewriteItem{{
|
||||||
Domain: "example.com",
|
Domain: "example.com",
|
||||||
Answer: "answer.com",
|
Answer: "answer.com",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Len(t, s.List(), 1)
|
require.Len(t, s.List(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultStorage_CRUD(t *testing.T) {
|
func TestDefaultStorage_CRUD(t *testing.T) {
|
||||||
var items []*Item
|
var items []*filtering.RewriteItem
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, s.List(), 0)
|
require.Len(t, s.List(), 0)
|
||||||
|
|
||||||
item := &Item{Domain: "example.com", Answer: "answer.com"}
|
item := &filtering.RewriteItem{Domain: "example.com", Answer: "answer.com"}
|
||||||
|
|
||||||
err = s.Add(item)
|
err = s.Add(item)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
list := s.List()
|
list := s.List()
|
||||||
require.Len(t, list, 1)
|
require.Len(t, list, 1)
|
||||||
require.True(t, item.equal(list[0]))
|
require.True(t, item.Equal(list[0]))
|
||||||
|
|
||||||
err = s.Remove(item)
|
err = s.Remove(item)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -45,7 +46,7 @@ func TestDefaultStorage_CRUD(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultStorage_MatchRequest(t *testing.T) {
|
func TestDefaultStorage_MatchRequest(t *testing.T) {
|
||||||
items := []*Item{{
|
items := []*filtering.RewriteItem{{
|
||||||
// This one and below are about CNAME, A and AAAA.
|
// This one and below are about CNAME, A and AAAA.
|
||||||
Domain: "somecname",
|
Domain: "somecname",
|
||||||
Answer: "somehost.com",
|
Answer: "somehost.com",
|
||||||
@@ -101,7 +102,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
Answer: "sub.issue4016.com",
|
Answer: "sub.issue4016.com",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -115,14 +116,39 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
wantDNSRewrites: nil,
|
wantDNSRewrites: nil,
|
||||||
dtyp: dns.TypeA,
|
dtyp: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "not_filtered_qtype",
|
name: "other_qtype",
|
||||||
host: "www.host.com",
|
host: "www.host.com",
|
||||||
wantDNSRewrites: nil,
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
dtyp: dns.TypeMX,
|
Value: nil,
|
||||||
|
NewCNAME: "host.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
|
Value: net.IP{1, 2, 3, 4}.To16(),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}, {
|
||||||
|
Value: net.IP{1, 2, 3, 5}.To16(),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}, {
|
||||||
|
Value: net.ParseIP("1:2:3::4"),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeAAAA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeMX,
|
||||||
}, {
|
}, {
|
||||||
name: "rewritten_a",
|
name: "rewritten_a",
|
||||||
host: "www.host.com",
|
host: "www.host.com",
|
||||||
wantDNSRewrites: []*rules.DNSRewrite{{
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "host.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
Value: net.IP{1, 2, 3, 4}.To16(),
|
Value: net.IP{1, 2, 3, 4}.To16(),
|
||||||
NewCNAME: "",
|
NewCNAME: "",
|
||||||
RCode: dns.RcodeSuccess,
|
RCode: dns.RcodeSuccess,
|
||||||
@@ -138,6 +164,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
name: "rewritten_aaaa",
|
name: "rewritten_aaaa",
|
||||||
host: "www.host.com",
|
host: "www.host.com",
|
||||||
wantDNSRewrites: []*rules.DNSRewrite{{
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "host.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
Value: net.ParseIP("1:2:3::4"),
|
Value: net.ParseIP("1:2:3::4"),
|
||||||
NewCNAME: "",
|
NewCNAME: "",
|
||||||
RCode: dns.RcodeSuccess,
|
RCode: dns.RcodeSuccess,
|
||||||
@@ -154,21 +185,30 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
RRType: dns.TypeA,
|
RRType: dns.TypeA,
|
||||||
}},
|
}},
|
||||||
dtyp: dns.TypeA,
|
dtyp: dns.TypeA,
|
||||||
//}, {
|
}, {
|
||||||
// TODO(d.kolyshev): This is about matching in urlfilter.
|
name: "wildcard_override",
|
||||||
// name: "wildcard_override",
|
host: "a.host.com",
|
||||||
// host: "a.host.com",
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
// wantDNSRewrites: []*rules.DNSRewrite{{
|
Value: net.IP{1, 2, 3, 4}.To16(),
|
||||||
// Value: net.IP{1, 2, 3, 4}.To16(),
|
NewCNAME: "",
|
||||||
// NewCNAME: "",
|
RCode: dns.RcodeSuccess,
|
||||||
// RCode: dns.RcodeSuccess,
|
RRType: dns.TypeA,
|
||||||
// RRType: dns.TypeA,
|
}, {
|
||||||
// }},
|
Value: net.IP{1, 2, 3, 5}.To16(),
|
||||||
// dtyp: dns.TypeA,
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "wildcard_cname_interaction",
|
name: "wildcard_cname_interaction",
|
||||||
host: "www.host2.com",
|
host: "www.host2.com",
|
||||||
wantDNSRewrites: []*rules.DNSRewrite{{
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "host.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
Value: net.IP{1, 2, 3, 4}.To16(),
|
Value: net.IP{1, 2, 3, 4}.To16(),
|
||||||
NewCNAME: "",
|
NewCNAME: "",
|
||||||
RCode: dns.RcodeSuccess,
|
RCode: dns.RcodeSuccess,
|
||||||
@@ -184,6 +224,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
name: "two_cnames",
|
name: "two_cnames",
|
||||||
host: "b.host.com",
|
host: "b.host.com",
|
||||||
wantDNSRewrites: []*rules.DNSRewrite{{
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "somehost.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
Value: net.IP{0, 0, 0, 0}.To16(),
|
Value: net.IP{0, 0, 0, 0}.To16(),
|
||||||
NewCNAME: "",
|
NewCNAME: "",
|
||||||
RCode: dns.RcodeSuccess,
|
RCode: dns.RcodeSuccess,
|
||||||
@@ -194,6 +239,11 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
name: "two_cnames_and_wildcard",
|
name: "two_cnames_and_wildcard",
|
||||||
host: "b.host3.com",
|
host: "b.host3.com",
|
||||||
wantDNSRewrites: []*rules.DNSRewrite{{
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "x.host.com",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
Value: net.IP{1, 2, 3, 5}.To16(),
|
Value: net.IP{1, 2, 3, 5}.To16(),
|
||||||
NewCNAME: "",
|
NewCNAME: "",
|
||||||
RCode: dns.RcodeSuccess,
|
RCode: dns.RcodeSuccess,
|
||||||
@@ -221,10 +271,15 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
dtyp: dns.TypeA,
|
dtyp: dns.TypeA,
|
||||||
}, {
|
}, {
|
||||||
name: "issue4008",
|
name: "issue4008",
|
||||||
host: "somehost.com",
|
host: "somehost.com",
|
||||||
wantDNSRewrites: nil,
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
dtyp: dns.TypeHTTPS,
|
Value: net.IP{0, 0, 0, 0}.To16(),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeHTTPS,
|
||||||
}, {
|
}, {
|
||||||
name: "issue4016",
|
name: "issue4016",
|
||||||
host: "www.issue4016.com",
|
host: "www.issue4016.com",
|
||||||
@@ -256,7 +311,7 @@ func TestDefaultStorage_MatchRequest(t *testing.T) {
|
|||||||
|
|
||||||
func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
|
func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
|
||||||
// Exact host, wildcard L2, wildcard L3.
|
// Exact host, wildcard L2, wildcard L3.
|
||||||
items := []*Item{{
|
items := []*filtering.RewriteItem{{
|
||||||
Domain: "host.com",
|
Domain: "host.com",
|
||||||
Answer: "1.1.1.1",
|
Answer: "1.1.1.1",
|
||||||
}, {
|
}, {
|
||||||
@@ -267,7 +322,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
|
|||||||
Answer: "3.3.3.3",
|
Answer: "3.3.3.3",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -295,17 +350,21 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
|
|||||||
RRType: dns.TypeA,
|
RRType: dns.TypeA,
|
||||||
}},
|
}},
|
||||||
dtyp: dns.TypeA,
|
dtyp: dns.TypeA,
|
||||||
//}, {
|
}, {
|
||||||
// TODO(d.kolyshev): This is about matching in urlfilter.
|
name: "l3_match",
|
||||||
// name: "l3_match",
|
host: "my.sub.host.com",
|
||||||
// host: "my.sub.host.com",
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
// wantDNSRewrites: []*rules.DNSRewrite{{
|
Value: net.IP{3, 3, 3, 3}.To16(),
|
||||||
// Value: net.IP{3, 3, 3, 3}.To16(),
|
NewCNAME: "",
|
||||||
// NewCNAME: "",
|
RCode: dns.RcodeSuccess,
|
||||||
// RCode: dns.RcodeSuccess,
|
RRType: dns.TypeA,
|
||||||
// RRType: dns.TypeA,
|
}, {
|
||||||
// }},
|
Value: net.IP{2, 2, 2, 2}.To16(),
|
||||||
// dtyp: dns.TypeA,
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeA,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -322,7 +381,7 @@ func TestDefaultStorage_MatchRequest_Levels(t *testing.T) {
|
|||||||
|
|
||||||
func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
|
func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
|
||||||
// Wildcard and exception for a sub-domain.
|
// Wildcard and exception for a sub-domain.
|
||||||
items := []*Item{{
|
items := []*filtering.RewriteItem{{
|
||||||
Domain: "*.host.com",
|
Domain: "*.host.com",
|
||||||
Answer: "2.2.2.2",
|
Answer: "2.2.2.2",
|
||||||
}, {
|
}, {
|
||||||
@@ -330,10 +389,10 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
|
|||||||
Answer: "sub.host.com",
|
Answer: "sub.host.com",
|
||||||
}, {
|
}, {
|
||||||
Domain: "*.sub.host.com",
|
Domain: "*.sub.host.com",
|
||||||
Answer: "*.sub.host.com",
|
Answer: "sub.host.com",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -356,12 +415,79 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
|
|||||||
host: "sub.host.com",
|
host: "sub.host.com",
|
||||||
wantDNSRewrites: nil,
|
wantDNSRewrites: nil,
|
||||||
dtyp: dns.TypeA,
|
dtyp: dns.TypeA,
|
||||||
//}, {
|
}, {
|
||||||
// TODO(d.kolyshev): This is about matching in urlfilter.
|
name: "exception_wildcard",
|
||||||
// name: "exception_wildcard",
|
host: "my.sub.host.com",
|
||||||
// host: "my.sub.host.com",
|
wantDNSRewrites: nil,
|
||||||
// wantDNSRewrites: nil,
|
dtyp: dns.TypeA,
|
||||||
// dtyp: dns.TypeA,
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
dnsRewrites := s.MatchRequest(&urlfilter.DNSRequest{
|
||||||
|
Hostname: tc.host,
|
||||||
|
DNSType: tc.dtyp,
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, tc.wantDNSRewrites, dnsRewrites)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultStorage_MatchRequest_CNAMEs(t *testing.T) {
|
||||||
|
// Two cname rules for one subdomain
|
||||||
|
items := []*filtering.RewriteItem{{
|
||||||
|
Domain: "cname.org",
|
||||||
|
Answer: "1.1.1.1",
|
||||||
|
}, {
|
||||||
|
Domain: "sub_cname.org",
|
||||||
|
Answer: "2.2.2.2",
|
||||||
|
}, {
|
||||||
|
Domain: "*.host.com",
|
||||||
|
Answer: "cname.org",
|
||||||
|
}, {
|
||||||
|
Domain: "*.sub.host.com",
|
||||||
|
Answer: "sub_cname.org",
|
||||||
|
}}
|
||||||
|
|
||||||
|
s, err := NewDefaultStorage(items)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
host string
|
||||||
|
wantDNSRewrites []*rules.DNSRewrite
|
||||||
|
dtyp uint16
|
||||||
|
}{{
|
||||||
|
name: "match_my_domain",
|
||||||
|
host: "my.host.com",
|
||||||
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "cname.org",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
|
Value: net.IP{1, 1, 1, 1}.To16(),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeA,
|
||||||
|
}, {
|
||||||
|
name: "match_sub_my_domain",
|
||||||
|
host: "my.sub.host.com",
|
||||||
|
wantDNSRewrites: []*rules.DNSRewrite{{
|
||||||
|
Value: nil,
|
||||||
|
NewCNAME: "cname.org",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeNone,
|
||||||
|
}, {
|
||||||
|
Value: net.IP{1, 1, 1, 1}.To16(),
|
||||||
|
NewCNAME: "",
|
||||||
|
RCode: dns.RcodeSuccess,
|
||||||
|
RRType: dns.TypeA,
|
||||||
|
}},
|
||||||
|
dtyp: dns.TypeA,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
@@ -378,7 +504,7 @@ func TestDefaultStorage_MatchRequest_ExceptionCNAME(t *testing.T) {
|
|||||||
|
|
||||||
func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
|
func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
|
||||||
// Exception for AAAA record.
|
// Exception for AAAA record.
|
||||||
items := []*Item{{
|
items := []*filtering.RewriteItem{{
|
||||||
Domain: "host.com",
|
Domain: "host.com",
|
||||||
Answer: "1.2.3.4",
|
Answer: "1.2.3.4",
|
||||||
}, {
|
}, {
|
||||||
@@ -395,7 +521,7 @@ func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
|
|||||||
Answer: "A",
|
Answer: "A",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
s, err := NewDefaultStorage(-1, items)
|
s, err := NewDefaultStorage(items)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
@@ -456,3 +582,66 @@ func TestDefaultStorage_MatchRequest_ExceptionIP(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToRule(t *testing.T) {
|
||||||
|
const testDomain = "example.org"
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
item *filtering.RewriteItem
|
||||||
|
want string
|
||||||
|
}{{
|
||||||
|
name: "nil",
|
||||||
|
item: nil,
|
||||||
|
want: "",
|
||||||
|
}, {
|
||||||
|
name: "a_rule",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: testDomain,
|
||||||
|
Answer: "1.1.1.1",
|
||||||
|
},
|
||||||
|
want: "|example.org^$dnsrewrite=NOERROR;A;1.1.1.1",
|
||||||
|
}, {
|
||||||
|
name: "aaaa_rule",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: testDomain,
|
||||||
|
Answer: "1:2:3::4",
|
||||||
|
},
|
||||||
|
want: "|example.org^$dnsrewrite=NOERROR;AAAA;1:2:3::4",
|
||||||
|
}, {
|
||||||
|
name: "cname_rule",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: testDomain,
|
||||||
|
Answer: "other.org",
|
||||||
|
},
|
||||||
|
want: "|example.org^$dnsrewrite=NOERROR;CNAME;other.org",
|
||||||
|
}, {
|
||||||
|
name: "wildcard_rule",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: "*.example.org",
|
||||||
|
Answer: "other.org",
|
||||||
|
},
|
||||||
|
want: "|*.example.org^$dnsrewrite=NOERROR;CNAME;other.org",
|
||||||
|
}, {
|
||||||
|
name: "aaaa_exception",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: testDomain,
|
||||||
|
Answer: "A",
|
||||||
|
},
|
||||||
|
want: "@@||example.org^$dnstype=A,dnsrewrite",
|
||||||
|
}, {
|
||||||
|
name: "aaaa_exception",
|
||||||
|
item: &filtering.RewriteItem{
|
||||||
|
Domain: testDomain,
|
||||||
|
Answer: "AAAA",
|
||||||
|
},
|
||||||
|
want: "@@||example.org^$dnstype=AAAA,dnsrewrite",
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
res := toRule(tc.item)
|
||||||
|
assert.Equal(t, tc.want, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
61
internal/filtering/rewrite_test.go
Normal file
61
internal/filtering/rewrite_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,85 +8,57 @@ import (
|
|||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO(d.kolyshev): Use [rewrite.Item] instead.
|
// handleRewriteList is the handler for the GET /control/rewrite/list HTTP API.
|
||||||
type rewriteEntryJSON struct {
|
|
||||||
Domain string `json:"domain"`
|
|
||||||
Answer string `json:"answer"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
||||||
arr := []*rewriteEntryJSON{}
|
_ = aghhttp.WriteJSONResponse(w, r, d.rewriteStorage.List())
|
||||||
|
|
||||||
d.confLock.Lock()
|
|
||||||
for _, ent := range d.Config.Rewrites {
|
|
||||||
jsent := rewriteEntryJSON{
|
|
||||||
Domain: ent.Domain,
|
|
||||||
Answer: ent.Answer,
|
|
||||||
}
|
|
||||||
arr = append(arr, &jsent)
|
|
||||||
}
|
|
||||||
d.confLock.Unlock()
|
|
||||||
|
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, arr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRewriteAdd is the handler for the POST /control/rewrite/add HTTP API.
|
||||||
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
rwJSON := rewriteEntryJSON{}
|
rw := &RewriteItem{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&rwJSON)
|
err := json.NewDecoder(r.Body).Decode(rw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rw := &LegacyRewrite{
|
err = d.rewriteStorage.Add(rw)
|
||||||
Domain: rwJSON.Domain,
|
|
||||||
Answer: rwJSON.Answer,
|
|
||||||
}
|
|
||||||
|
|
||||||
err = rw.normalize()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Shouldn't happen currently, since normalize only returns a non-nil
|
aghhttp.Error(r, w, http.StatusBadRequest, "add rewrite: %s", err)
|
||||||
// error when a rewrite is nil, but be change-proof.
|
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "normalizing: %s", err)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug("rewrite: added element: %s -> %s", rw.Domain, rw.Answer)
|
||||||
|
|
||||||
d.confLock.Lock()
|
d.confLock.Lock()
|
||||||
d.Config.Rewrites = append(d.Config.Rewrites, rw)
|
d.Config.Rewrites = d.rewriteStorage.List()
|
||||||
d.confLock.Unlock()
|
d.confLock.Unlock()
|
||||||
log.Debug("rewrite: added element: %s -> %s [%d]", rw.Domain, rw.Answer, len(d.Config.Rewrites))
|
|
||||||
|
|
||||||
d.Config.ConfigModified()
|
d.Config.ConfigModified()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handleRewriteDelete is the handler for the POST /control/rewrite/delete HTTP
|
||||||
|
// API.
|
||||||
func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteDelete(w http.ResponseWriter, r *http.Request) {
|
||||||
jsent := rewriteEntryJSON{}
|
entDel := RewriteItem{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&jsent)
|
err := json.NewDecoder(r.Body).Decode(&entDel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "json.Decode: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
entDel := &LegacyRewrite{
|
err = d.rewriteStorage.Remove(&entDel)
|
||||||
Domain: jsent.Domain,
|
if err != nil {
|
||||||
Answer: jsent.Answer,
|
aghhttp.Error(r, w, http.StatusBadRequest, "remove rewrite: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
arr := []*LegacyRewrite{}
|
|
||||||
|
|
||||||
d.confLock.Lock()
|
d.confLock.Lock()
|
||||||
for _, ent := range d.Config.Rewrites {
|
d.Config.Rewrites = d.rewriteStorage.List()
|
||||||
if ent.equal(entDel) {
|
|
||||||
log.Debug("rewrite: removed element: %s -> %s", ent.Domain, ent.Answer)
|
|
||||||
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
arr = append(arr, ent)
|
|
||||||
}
|
|
||||||
d.Config.Rewrites = arr
|
|
||||||
d.confLock.Unlock()
|
d.confLock.Unlock()
|
||||||
|
|
||||||
d.Config.ConfigModified()
|
d.Config.ConfigModified()
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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]))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -253,7 +253,6 @@ var blockedServices = []blockedService{{
|
|||||||
Rules: []string{
|
Rules: []string{
|
||||||
"||aus.social^",
|
"||aus.social^",
|
||||||
"||awscommunity.social^",
|
"||awscommunity.social^",
|
||||||
"||dju.social^",
|
|
||||||
"||dresden.network^",
|
"||dresden.network^",
|
||||||
"||fedibird.com^",
|
"||fedibird.com^",
|
||||||
"||fosstodon.org^",
|
"||fosstodon.org^",
|
||||||
@@ -261,11 +260,11 @@ var blockedServices = []blockedService{{
|
|||||||
"||h4.io^",
|
"||h4.io^",
|
||||||
"||hachyderm.io^",
|
"||hachyderm.io^",
|
||||||
"||hessen.social^",
|
"||hessen.social^",
|
||||||
"||hispagatos.space^",
|
|
||||||
"||home.social^",
|
"||home.social^",
|
||||||
"||hostux.social^",
|
"||hostux.social^",
|
||||||
"||ieji.de^",
|
"||ieji.de^",
|
||||||
"||indieweb.social^",
|
"||indieweb.social^",
|
||||||
|
"||infosec.exchange^",
|
||||||
"||ioc.exchange^",
|
"||ioc.exchange^",
|
||||||
"||kolektiva.social^",
|
"||kolektiva.social^",
|
||||||
"||livellosegreto.it^",
|
"||livellosegreto.it^",
|
||||||
@@ -287,9 +286,11 @@ var blockedServices = []blockedService{{
|
|||||||
"||mastodon.nu^",
|
"||mastodon.nu^",
|
||||||
"||mastodon.nz^",
|
"||mastodon.nz^",
|
||||||
"||mastodon.online^",
|
"||mastodon.online^",
|
||||||
|
"||mastodon.online^",
|
||||||
"||mastodon.scot^",
|
"||mastodon.scot^",
|
||||||
"||mastodon.sdf.org^",
|
"||mastodon.sdf.org^",
|
||||||
"||mastodon.social^",
|
"||mastodon.social^",
|
||||||
|
"||mastodon.social^",
|
||||||
"||mastodon.top^",
|
"||mastodon.top^",
|
||||||
"||mastodon.uno^",
|
"||mastodon.uno^",
|
||||||
"||mastodon.world^",
|
"||mastodon.world^",
|
||||||
@@ -309,7 +310,6 @@ var blockedServices = []blockedService{{
|
|||||||
"||mstdn.social^",
|
"||mstdn.social^",
|
||||||
"||muenchen.social^",
|
"||muenchen.social^",
|
||||||
"||muenster.im^",
|
"||muenster.im^",
|
||||||
"||nerdculture.de^",
|
|
||||||
"||newsie.social^",
|
"||newsie.social^",
|
||||||
"||noc.social^",
|
"||noc.social^",
|
||||||
"||norden.social^",
|
"||norden.social^",
|
||||||
@@ -335,21 +335,21 @@ var blockedServices = []blockedService{{
|
|||||||
"||social.vivaldi.net^",
|
"||social.vivaldi.net^",
|
||||||
"||sself.co^",
|
"||sself.co^",
|
||||||
"||sueden.social^",
|
"||sueden.social^",
|
||||||
|
"||tech.lgbt^",
|
||||||
"||techhub.social^",
|
"||techhub.social^",
|
||||||
"||theblower.au^",
|
"||theblower.au^",
|
||||||
"||tkz.one^",
|
"||tkz.one^",
|
||||||
|
"||todon.eu^",
|
||||||
"||toot.aquilenet.fr^",
|
"||toot.aquilenet.fr^",
|
||||||
"||toot.community^",
|
"||toot.community^",
|
||||||
"||toot.funami.tech^",
|
"||toot.funami.tech^",
|
||||||
"||toot.wales^",
|
"||toot.wales^",
|
||||||
"||troet.cafe^",
|
"||troet.cafe^",
|
||||||
"||uiuxdev.social^",
|
"||twingyeo.kr^",
|
||||||
"||union.place^",
|
"||union.place^",
|
||||||
"||universeodon.com^",
|
"||universeodon.com^",
|
||||||
"||urbanists.social^",
|
"||urbanists.social^",
|
||||||
"||vocalodon.net^",
|
|
||||||
"||wxw.moe^",
|
"||wxw.moe^",
|
||||||
"||xarxa.cloud^",
|
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
ID: "minecraft",
|
ID: "minecraft",
|
||||||
@@ -540,6 +540,7 @@ var blockedServices = []blockedService{{
|
|||||||
Name: "Twitter",
|
Name: "Twitter",
|
||||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0\" /></svg>"),
|
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\"><path d=\"M22.398 5.55a8.583 8.583 0 0 1-2.449.673 4.252 4.252 0 0 0 1.875-2.364 8.66 8.66 0 0 1-2.71 1.04A4.251 4.251 0 0 0 16 3.546a4.27 4.27 0 0 0-4.266 4.27c0 .335.036.66.11.972a12.126 12.126 0 0 1-8.797-4.46 4.259 4.259 0 0 0-.578 2.148c0 1.48.754 2.785 1.898 3.55a4.273 4.273 0 0 1-1.933-.535v.055a4.27 4.27 0 0 0 3.425 4.183c-.359.098-.734.149-1.125.149-.273 0-.543-.027-.804-.074a4.276 4.276 0 0 0 3.988 2.965 8.562 8.562 0 0 1-5.3 1.824 8.82 8.82 0 0 1-1.02-.059 12.088 12.088 0 0 0 6.543 1.918c7.851 0 12.14-6.504 12.14-12.144 0-.184-.004-.368-.011-.551a8.599 8.599 0 0 0 2.128-2.207zm0 0\" /></svg>"),
|
||||||
Rules: []string{
|
Rules: []string{
|
||||||
|
"||pscp.tv^",
|
||||||
"||t.co^",
|
"||t.co^",
|
||||||
"||twimg.com^",
|
"||twimg.com^",
|
||||||
"||twitter.com^",
|
"||twitter.com^",
|
||||||
|
|||||||
@@ -278,15 +278,20 @@ var config = &configuration{
|
|||||||
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
|
PortDNSOverTLS: defaultPortTLS, // needs to be passed through to dnsproxy
|
||||||
PortDNSOverQUIC: defaultPortQUIC,
|
PortDNSOverQUIC: defaultPortQUIC,
|
||||||
},
|
},
|
||||||
|
// NOTE: Keep these parameters in sync with the one put into
|
||||||
|
// client/src/helpers/filters/filters.js by scripts/vetted-filters.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Think of a way to make scripts/vetted-filters update
|
||||||
|
// these as well if necessary.
|
||||||
Filters: []filtering.FilterYAML{{
|
Filters: []filtering.FilterYAML{{
|
||||||
Filter: filtering.Filter{ID: 1},
|
Filter: filtering.Filter{ID: 1},
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt",
|
URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt",
|
||||||
Name: "AdGuard DNS filter",
|
Name: "AdGuard DNS filter",
|
||||||
}, {
|
}, {
|
||||||
Filter: filtering.Filter{ID: 2},
|
Filter: filtering.Filter{ID: 2},
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
URL: "https://adaway.org/hosts.txt",
|
URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt",
|
||||||
Name: "AdAway Default Blocklist",
|
Name: "AdAway Default Blocklist",
|
||||||
}},
|
}},
|
||||||
DHCP: &dhcpd.ServerConfig{
|
DHCP: &dhcpd.ServerConfig{
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.updater.Update()
|
err = Context.updater.Update(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
aghhttp.Error(r, w, http.StatusInternalServerError, "%s", err)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
"github.com/AdguardTeam/AdGuardHome/internal/dnsforward"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering/rewrite"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
@@ -39,17 +42,13 @@ func onConfigModified() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initDNSServer creates an instance of the dnsforward.Server
|
// initDNS updates all the fields of the [Context] needed to initialize the DNS
|
||||||
// Please note that we must do it even if we don't start it
|
// server and initializes it at last. It also must not be called unless
|
||||||
// so that we had access to the query log and the stats
|
// [config] and [Context] are initialized.
|
||||||
func initDNSServer() (err error) {
|
func initDNS() (err error) {
|
||||||
baseDir := Context.getDataDir()
|
baseDir := Context.getDataDir()
|
||||||
|
|
||||||
var anonFunc aghnet.IPMutFunc
|
anonymizer := config.anonymizer()
|
||||||
if config.DNS.AnonymizeClientIP {
|
|
||||||
anonFunc = querylog.AnonymizeIP
|
|
||||||
}
|
|
||||||
anonymizer := aghnet.NewIPMut(anonFunc)
|
|
||||||
|
|
||||||
statsConf := stats.Config{
|
statsConf := stats.Config{
|
||||||
Filename: filepath.Join(baseDir, "stats.db"),
|
Filename: filepath.Join(baseDir, "stats.db"),
|
||||||
@@ -76,40 +75,57 @@ func initDNSServer() (err error) {
|
|||||||
}
|
}
|
||||||
Context.queryLog = querylog.New(conf)
|
Context.queryLog = querylog.New(conf)
|
||||||
|
|
||||||
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil)
|
rewriteStorage, err := rewrite.NewDefaultStorage(config.DNS.DnsfilterConf.Rewrites)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("rewrites: init: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.filters, err = filtering.New(config.DNS.DnsfilterConf, nil, rewriteStorage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error, since it's informative enough as is.
|
// Don't wrap the error, since it's informative enough as is.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var privateNets netutil.SubnetSet
|
tlsConf := &tlsConfigSettings{}
|
||||||
switch len(config.DNS.PrivateNets) {
|
Context.tls.WriteDiskConfig(tlsConf)
|
||||||
case 0:
|
|
||||||
// Use an optimized locally-served matcher.
|
|
||||||
privateNets = netutil.SubnetSetFunc(netutil.IsLocallyServed)
|
|
||||||
case 1:
|
|
||||||
privateNets, err = netutil.ParseSubnet(config.DNS.PrivateNets[0])
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
var nets []*net.IPNet
|
|
||||||
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("preparing the set of private subnets: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
privateNets = netutil.SliceSubnetSet(nets)
|
return initDNSServer(
|
||||||
|
Context.filters,
|
||||||
|
Context.stats,
|
||||||
|
Context.queryLog,
|
||||||
|
Context.dhcpServer,
|
||||||
|
anonymizer,
|
||||||
|
httpRegister,
|
||||||
|
tlsConf,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// initDNSServer initializes the [context.dnsServer]. To only use the internal
|
||||||
|
// proxy, none of the arguments are required, but tlsConf still must not be nil,
|
||||||
|
// in other cases all the arguments also must not be nil. It also must not be
|
||||||
|
// called unless [config] and [Context] are initialized.
|
||||||
|
func initDNSServer(
|
||||||
|
filters *filtering.DNSFilter,
|
||||||
|
sts stats.Interface,
|
||||||
|
qlog querylog.QueryLog,
|
||||||
|
dhcpSrv dhcpd.Interface,
|
||||||
|
anonymizer *aghnet.IPMut,
|
||||||
|
httpReg aghhttp.RegisterFunc,
|
||||||
|
tlsConf *tlsConfigSettings,
|
||||||
|
) (err error) {
|
||||||
|
privateNets, err := parseSubnetSet(config.DNS.PrivateNets)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("preparing set of private subnets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
p := dnsforward.DNSCreateParams{
|
p := dnsforward.DNSCreateParams{
|
||||||
DNSFilter: Context.filters,
|
DNSFilter: filters,
|
||||||
Stats: Context.stats,
|
Stats: sts,
|
||||||
QueryLog: Context.queryLog,
|
QueryLog: qlog,
|
||||||
PrivateNets: privateNets,
|
PrivateNets: privateNets,
|
||||||
Anonymizer: anonymizer,
|
Anonymizer: anonymizer,
|
||||||
LocalDomain: config.DHCP.LocalDomainName,
|
LocalDomain: config.DHCP.LocalDomainName,
|
||||||
DHCPServer: Context.dhcpServer,
|
DHCPServer: dhcpSrv,
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.dnsServer, err = dnsforward.NewServer(p)
|
Context.dnsServer, err = dnsforward.NewServer(p)
|
||||||
@@ -120,15 +136,15 @@ func initDNSServer() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Context.clients.dnsServer = Context.dnsServer
|
Context.clients.dnsServer = Context.dnsServer
|
||||||
var dnsConfig dnsforward.ServerConfig
|
|
||||||
dnsConfig, err = generateServerConfig()
|
dnsConf, err := generateServerConfig(tlsConf, httpReg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
closeDNSServer()
|
||||||
|
|
||||||
return fmt.Errorf("generateServerConfig: %w", err)
|
return fmt.Errorf("generateServerConfig: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.dnsServer.Prepare(&dnsConfig)
|
err = Context.dnsServer.Prepare(&dnsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
closeDNSServer()
|
||||||
|
|
||||||
@@ -146,6 +162,32 @@ func initDNSServer() (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSubnetSet parses a slice of subnets. If the slice is empty, it returns
|
||||||
|
// a subnet set that matches all locally served networks, see
|
||||||
|
// [netutil.IsLocallyServed].
|
||||||
|
func parseSubnetSet(nets []string) (s netutil.SubnetSet, err error) {
|
||||||
|
switch len(nets) {
|
||||||
|
case 0:
|
||||||
|
// Use an optimized function-based matcher.
|
||||||
|
return netutil.SubnetSetFunc(netutil.IsLocallyServed), nil
|
||||||
|
case 1:
|
||||||
|
s, err = netutil.ParseSubnet(nets[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
default:
|
||||||
|
var nets []*net.IPNet
|
||||||
|
nets, err = netutil.ParseSubnets(config.DNS.PrivateNets...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return netutil.SliceSubnetSet(nets), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func isRunning() bool {
|
func isRunning() bool {
|
||||||
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
return Context.dnsServer != nil && Context.dnsServer.IsRunning()
|
||||||
}
|
}
|
||||||
@@ -193,7 +235,10 @@ func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
|
|||||||
return udpAddrs
|
return udpAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
func generateServerConfig(
|
||||||
|
tlsConf *tlsConfigSettings,
|
||||||
|
httpReg aghhttp.RegisterFunc,
|
||||||
|
) (newConf dnsforward.ServerConfig, err error) {
|
||||||
dnsConf := config.DNS
|
dnsConf := config.DNS
|
||||||
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
|
hosts := aghalg.CoalesceSlice(dnsConf.BindHosts, []netip.Addr{netutil.IPv4Localhost()})
|
||||||
newConf = dnsforward.ServerConfig{
|
newConf = dnsforward.ServerConfig{
|
||||||
@@ -201,12 +246,10 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
|||||||
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
TCPListenAddrs: ipsToTCPAddrs(hosts, dnsConf.Port),
|
||||||
FilteringConfig: dnsConf.FilteringConfig,
|
FilteringConfig: dnsConf.FilteringConfig,
|
||||||
ConfigModified: onConfigModified,
|
ConfigModified: onConfigModified,
|
||||||
HTTPRegister: httpRegister,
|
HTTPRegister: httpReg,
|
||||||
OnDNSRequest: onDNSRequest,
|
OnDNSRequest: onDNSRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
tlsConf := tlsConfigSettings{}
|
|
||||||
Context.tls.WriteDiskConfig(&tlsConf)
|
|
||||||
if tlsConf.Enabled {
|
if tlsConf.Enabled {
|
||||||
newConf.TLSConfig = tlsConf.TLSConfig
|
newConf.TLSConfig = tlsConf.TLSConfig
|
||||||
newConf.TLSConfig.ServerName = tlsConf.ServerName
|
newConf.TLSConfig.ServerName = tlsConf.ServerName
|
||||||
@@ -224,7 +267,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if tlsConf.PortDNSCrypt != 0 {
|
if tlsConf.PortDNSCrypt != 0 {
|
||||||
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, tlsConf)
|
newConf.DNSCryptConfig, err = newDNSCrypt(hosts, *tlsConf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap the error, because it's already
|
// Don't wrap the error, because it's already
|
||||||
// wrapped by newDNSCrypt.
|
// wrapped by newDNSCrypt.
|
||||||
@@ -413,7 +456,11 @@ func startDNSServer() error {
|
|||||||
|
|
||||||
func reconfigureDNSServer() (err error) {
|
func reconfigureDNSServer() (err error) {
|
||||||
var newConf dnsforward.ServerConfig
|
var newConf dnsforward.ServerConfig
|
||||||
newConf, err = generateServerConfig()
|
|
||||||
|
tlsConf := &tlsConfigSettings{}
|
||||||
|
Context.tls.WriteDiskConfig(tlsConf)
|
||||||
|
|
||||||
|
newConf, err = generateServerConfig(tlsConf, httpRegister)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("generating forwarding dns server config: %w", err)
|
return fmt.Errorf("generating forwarding dns server config: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -455,6 +455,10 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
err = setupConfig(opts)
|
err = setupConfig(opts)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
|
// TODO(e.burkov): This could be made earlier, probably as the option's
|
||||||
|
// effect.
|
||||||
|
cmdlineUpdate(opts)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
// Save the updated config
|
// Save the updated config
|
||||||
err = config.write()
|
err = config.write()
|
||||||
@@ -522,7 +526,7 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err = initDNSServer()
|
err = initDNS()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
Context.tls.start()
|
Context.tls.start()
|
||||||
@@ -543,20 +547,24 @@ func run(opts options, clientBuildFS fs.FS) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(a.garipov): This could be made much earlier and could be done on
|
|
||||||
// the first run as well, but to achieve this we need to bypass requests
|
|
||||||
// over dnsforward resolver.
|
|
||||||
cmdlineUpdate(opts)
|
|
||||||
|
|
||||||
Context.web.Start()
|
Context.web.Start()
|
||||||
|
|
||||||
// wait indefinitely for other go-routines to complete their job
|
// wait indefinitely for other go-routines to complete their job
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *configuration) anonymizer() (ipmut *aghnet.IPMut) {
|
||||||
|
var anonFunc aghnet.IPMutFunc
|
||||||
|
if c.DNS.AnonymizeClientIP {
|
||||||
|
anonFunc = querylog.AnonymizeIP
|
||||||
|
}
|
||||||
|
|
||||||
|
return aghnet.NewIPMut(anonFunc)
|
||||||
|
}
|
||||||
|
|
||||||
// startMods initializes and starts the DNS server after installation.
|
// startMods initializes and starts the DNS server after installation.
|
||||||
func startMods() error {
|
func startMods() (err error) {
|
||||||
err := initDNSServer()
|
err = initDNS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -927,8 +935,8 @@ func getHTTPProxy(_ *http.Request) (*url.URL, error) {
|
|||||||
|
|
||||||
// jsonError is a generic JSON error response.
|
// jsonError is a generic JSON error response.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Merge together with the implementations in .../dhcpd and
|
// TODO(a.garipov): Merge together with the implementations in [dhcpd] and other
|
||||||
// other packages after refactoring the web handler registering.
|
// packages after refactoring the web handler registering.
|
||||||
type jsonError struct {
|
type jsonError struct {
|
||||||
// Message is the error message, an opaque string.
|
// Message is the error message, an opaque string.
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -940,30 +948,40 @@ func cmdlineUpdate(opts options) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("starting update")
|
// Initialize the DNS server to use the internal resolver which the updater
|
||||||
|
// needs to be able to resolve the update source hostname.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): We could probably initialize the internal resolver
|
||||||
|
// separately.
|
||||||
|
err := initDNSServer(nil, nil, nil, nil, nil, nil, &tlsConfigSettings{})
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
if Context.firstRun {
|
log.Info("cmdline update: performing update")
|
||||||
log.Info("update not allowed on first run")
|
|
||||||
|
|
||||||
os.Exit(0)
|
updater := Context.updater
|
||||||
}
|
info, err := updater.VersionInfo(true)
|
||||||
|
|
||||||
_, err := Context.updater.VersionInfo(true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
vcu := Context.updater.VersionCheckURL()
|
vcu := updater.VersionCheckURL()
|
||||||
log.Error("getting version info from %s: %s", vcu, err)
|
log.Error("getting version info from %s: %s", vcu, err)
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.updater.NewVersion() == "" {
|
if info.NewVersion == version.Version() {
|
||||||
log.Info("no updates available")
|
log.Info("no updates available")
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = Context.updater.Update()
|
err = updater.Update(Context.firstRun)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
|
err = restartService()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug("restarting service: %s", err)
|
||||||
|
log.Info("AdGuard Home was not installed as a service. " +
|
||||||
|
"Please restart running instances of AdGuardHome manually.")
|
||||||
|
}
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,7 +229,7 @@ var cmdLineOpts = []cmdLineOpt{{
|
|||||||
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
|
updateNoValue: func(o options) (options, error) { o.performUpdate = true; return o, nil },
|
||||||
effect: nil,
|
effect: nil,
|
||||||
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
|
serialize: func(o options) (val string, ok bool) { return "", o.performUpdate },
|
||||||
description: "Update application and exit.",
|
description: "Update the current binary and restart the service in case it's installed.",
|
||||||
longName: "update",
|
longName: "update",
|
||||||
shortName: "",
|
shortName: "",
|
||||||
}, {
|
}, {
|
||||||
|
|||||||
@@ -159,6 +159,38 @@ func sendSigReload() {
|
|||||||
log.Debug("service: sent signal to pid %d", pid)
|
log.Debug("service: sent signal to pid %d", pid)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// restartService restarts the service. It returns error if the service is not
|
||||||
|
// running.
|
||||||
|
func restartService() (err error) {
|
||||||
|
// Call chooseSystem explicitly to introduce OpenBSD support for service
|
||||||
|
// package. It's a noop for other GOOS values.
|
||||||
|
chooseSystem()
|
||||||
|
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting current directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
svcConfig := &service.Config{
|
||||||
|
Name: serviceName,
|
||||||
|
DisplayName: serviceDisplayName,
|
||||||
|
Description: serviceDescription,
|
||||||
|
WorkingDirectory: pwd,
|
||||||
|
}
|
||||||
|
configureService(svcConfig)
|
||||||
|
|
||||||
|
var s service.Service
|
||||||
|
if s, err = service.New(&program{}, svcConfig); err != nil {
|
||||||
|
return fmt.Errorf("initializing service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = svcAction(s, "restart"); err != nil {
|
||||||
|
return fmt.Errorf("restarting service: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleServiceControlAction one of the possible control actions:
|
// handleServiceControlAction one of the possible control actions:
|
||||||
//
|
//
|
||||||
// - install: Installs a service/daemon.
|
// - install: Installs a service/daemon.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"github.com/kardianos/service"
|
"github.com/kardianos/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// chooseSystem checks the current system detected and substitutes it with local
|
||||||
|
// implementation if needed.
|
||||||
func chooseSystem() {
|
func chooseSystem() {
|
||||||
sys := service.ChosenSystem()
|
sys := service.ChosenSystem()
|
||||||
// By default, package service uses the SysV system if it cannot detect
|
// By default, package service uses the SysV system if it cannot detect
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ import (
|
|||||||
// sysVersion is the version of local service.System interface implementation.
|
// sysVersion is the version of local service.System interface implementation.
|
||||||
const sysVersion = "openbsd-runcom"
|
const sysVersion = "openbsd-runcom"
|
||||||
|
|
||||||
|
// chooseSystem checks the current system detected and substitutes it with local
|
||||||
|
// implementation if needed.
|
||||||
func chooseSystem() {
|
func chooseSystem() {
|
||||||
service.ChooseSystem(openbsdSystem{})
|
service.ChooseSystem(openbsdSystem{})
|
||||||
}
|
}
|
||||||
|
|||||||
63
internal/next/agh/agh.go
Normal file
63
internal/next/agh/agh.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||||
|
package agh
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Service is the interface for API servers.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider adding a context to Start.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
||||||
|
// interface for that.
|
||||||
|
type Service interface {
|
||||||
|
// Start starts the service. It does not block.
|
||||||
|
Start() (err error)
|
||||||
|
|
||||||
|
// Shutdown gracefully stops the service. ctx is used to determine
|
||||||
|
// a timeout before trying to stop the service less gracefully.
|
||||||
|
Shutdown(ctx context.Context) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ Service = EmptyService{}
|
||||||
|
|
||||||
|
// EmptyService is a [Service] that does nothing.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
|
type EmptyService struct{}
|
||||||
|
|
||||||
|
// Start implements the [Service] interface for EmptyService.
|
||||||
|
func (EmptyService) Start() (err error) { return nil }
|
||||||
|
|
||||||
|
// Shutdown implements the [Service] interface for EmptyService.
|
||||||
|
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||||
|
|
||||||
|
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||||
|
// that can return their configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||||
|
// how to make it testable in a better way.
|
||||||
|
type ServiceWithConfig[ConfigType any] interface {
|
||||||
|
Service
|
||||||
|
|
||||||
|
Config() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
|
||||||
|
// method returns Conf.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
|
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||||
|
EmptyService
|
||||||
|
|
||||||
|
Conf ConfigType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [ServiceWithConfig] interface for
|
||||||
|
// *EmptyServiceWithConfig.
|
||||||
|
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
|
||||||
|
return s.Conf
|
||||||
|
}
|
||||||
77
internal/next/cmd/cmd.go
Normal file
77
internal/next/cmd/cmd.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Package cmd is the AdGuard Home entry point. It contains the on-disk
|
||||||
|
// configuration file utilities, signal processing logic, and so on.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to the upper-level internal/.
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/fs"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Main is the entry point of application.
|
||||||
|
func Main(clientBuildFS fs.FS) {
|
||||||
|
// Initial Configuration
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
rand.Seed(start.UnixNano())
|
||||||
|
|
||||||
|
// TODO(a.garipov): Set up logging.
|
||||||
|
|
||||||
|
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||||
|
|
||||||
|
// Web Service
|
||||||
|
|
||||||
|
// TODO(a.garipov): Use in the Web service.
|
||||||
|
_ = clientBuildFS
|
||||||
|
|
||||||
|
// TODO(a.garipov): Set up configuration file name.
|
||||||
|
const confFile = "AdGuardHome.1.yaml"
|
||||||
|
|
||||||
|
confMgr, err := configmgr.New(confFile, start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
sigHdlr := newSignalHandler(
|
||||||
|
confFile,
|
||||||
|
start,
|
||||||
|
web,
|
||||||
|
dns,
|
||||||
|
)
|
||||||
|
|
||||||
|
go sigHdlr.handle()
|
||||||
|
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// defaultTimeout is the timeout used for some operations where another timeout
|
||||||
|
// hasn't been defined yet.
|
||||||
|
const defaultTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
// ctxWithDefaultTimeout is a helper function that returns a context with
|
||||||
|
// timeout set to defaultTimeout.
|
||||||
|
func ctxWithDefaultTimeout() (ctx context.Context, cancel context.CancelFunc) {
|
||||||
|
return context.WithTimeout(context.Background(), defaultTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fatalOnError is a helper that exits the program with an error code if err is
|
||||||
|
// not nil. It must only be used within Main.
|
||||||
|
func fatalOnError(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
118
internal/next/cmd/signal.go
Normal file
118
internal/next/cmd/signal.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// signalHandler processes incoming signals and shuts services down.
|
||||||
|
type signalHandler struct {
|
||||||
|
// signal is the channel to which OS signals are sent.
|
||||||
|
signal chan os.Signal
|
||||||
|
|
||||||
|
// confFile is the path to the configuration file.
|
||||||
|
confFile string
|
||||||
|
|
||||||
|
// start is the time at which AdGuard Home has been started.
|
||||||
|
start time.Time
|
||||||
|
|
||||||
|
// services are the services that are shut down before application exiting.
|
||||||
|
services []agh.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle processes OS signals.
|
||||||
|
func (h *signalHandler) handle() {
|
||||||
|
defer log.OnPanic("signalHandler.handle")
|
||||||
|
|
||||||
|
for sig := range h.signal {
|
||||||
|
log.Info("sighdlr: received signal %q", sig)
|
||||||
|
|
||||||
|
if aghos.IsReconfigureSignal(sig) {
|
||||||
|
h.reconfigure()
|
||||||
|
} else if aghos.IsShutdownSignal(sig) {
|
||||||
|
status := h.shutdown()
|
||||||
|
log.Info("sighdlr: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconfigure rereads the configuration file and updates and restarts services.
|
||||||
|
func (h *signalHandler) reconfigure() {
|
||||||
|
log.Info("sighdlr: reconfiguring adguard home")
|
||||||
|
|
||||||
|
status := h.shutdown()
|
||||||
|
if status != statusSuccess {
|
||||||
|
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
||||||
|
// reconfigured without the full shutdown, and the error handling is
|
||||||
|
// currently not the best.
|
||||||
|
|
||||||
|
confMgr, err := configmgr.New(h.confFile, h.start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
h.services = []agh.Service{
|
||||||
|
dns,
|
||||||
|
web,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit status constants.
|
||||||
|
const (
|
||||||
|
statusSuccess = 0
|
||||||
|
statusError = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// shutdown gracefully shuts down all services.
|
||||||
|
func (h *signalHandler) shutdown() (status int) {
|
||||||
|
ctx, cancel := ctxWithDefaultTimeout()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status = statusSuccess
|
||||||
|
|
||||||
|
log.Info("sighdlr: shutting down services")
|
||||||
|
for i, service := range h.services {
|
||||||
|
err := service.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||||
|
status = statusError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||||
|
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
|
||||||
|
h = &signalHandler{
|
||||||
|
signal: make(chan os.Signal, 1),
|
||||||
|
confFile: confFile,
|
||||||
|
start: start,
|
||||||
|
services: svcs,
|
||||||
|
}
|
||||||
|
|
||||||
|
aghos.NotifyShutdownSignal(h.signal)
|
||||||
|
aghos.NotifyReconfigureSignal(h.signal)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
||||||
40
internal/next/configmgr/config.go
Normal file
40
internal/next/configmgr/config.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Structures
|
||||||
|
|
||||||
|
// config is the top-level on-disk configuration structure.
|
||||||
|
type config struct {
|
||||||
|
DNS *dnsConfig `yaml:"dns"`
|
||||||
|
HTTP *httpConfig `yaml:"http"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
SchemaVersion int `yaml:"schema_version"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
DebugPprof bool `yaml:"debug_pprof"`
|
||||||
|
Verbose bool `yaml:"verbose"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsConfig is the on-disk DNS configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type dnsConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||||
|
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||||
|
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpConfig is the on-disk web API configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type httpConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||||
|
Timeout timeutil.Duration `yaml:"timeout"`
|
||||||
|
ForceHTTPS bool `yaml:"force_https"`
|
||||||
|
}
|
||||||
205
internal/next/configmgr/configmgr.go
Normal file
205
internal/next/configmgr/configmgr.go
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||||
|
// configuration manager.
|
||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Manager
|
||||||
|
|
||||||
|
// Manager handles full and partial changes in the configuration, persisting
|
||||||
|
// them to disk if necessary.
|
||||||
|
type Manager struct {
|
||||||
|
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||||
|
// updMu protects all fields below.
|
||||||
|
updMu *sync.RWMutex
|
||||||
|
|
||||||
|
// dns is the DNS service.
|
||||||
|
dns *dnssvc.Service
|
||||||
|
|
||||||
|
// Web is the Web API service.
|
||||||
|
web *websvc.Service
|
||||||
|
|
||||||
|
// current is the current configuration.
|
||||||
|
current *config
|
||||||
|
|
||||||
|
// fileName is the name of the configuration file.
|
||||||
|
fileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new *Manager that persists changes to the file pointed to by
|
||||||
|
// fileName. It reads the configuration file and populates the service fields.
|
||||||
|
// start is the startup time of AdGuard Home.
|
||||||
|
func New(fileName string, start time.Time) (m *Manager, err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
f, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||||
|
|
||||||
|
err = yaml.NewDecoder(f).Decode(conf)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Move into a separate function and add other logging
|
||||||
|
// settings.
|
||||||
|
if conf.Verbose {
|
||||||
|
log.SetLevel(log.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||||
|
// if it's incorrect.
|
||||||
|
|
||||||
|
m = &Manager{
|
||||||
|
updMu: &sync.RWMutex{},
|
||||||
|
current: conf,
|
||||||
|
fileName: fileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||||
|
const assemblyTimeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = m.assemble(ctx, conf, start)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble creates all services and puts them into the corresponding fields.
|
||||||
|
// The fields of conf must not be modified after calling assemble.
|
||||||
|
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
|
||||||
|
dnsConf := &dnssvc.Config{
|
||||||
|
Addresses: conf.DNS.Addresses,
|
||||||
|
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||||
|
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||||
|
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
|
||||||
|
}
|
||||||
|
err = m.updateDNS(ctx, dnsConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
webSvcConf := &websvc.Config{
|
||||||
|
ConfigManager: m,
|
||||||
|
// TODO(a.garipov): Fill from config file.
|
||||||
|
TLS: nil,
|
||||||
|
Start: start,
|
||||||
|
Addresses: conf.HTTP.Addresses,
|
||||||
|
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||||
|
Timeout: conf.HTTP.Timeout.Duration,
|
||||||
|
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, webSvcConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.dns
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateDNS.
|
||||||
|
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateDNS(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||||
|
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
if prev := m.dns; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down dns svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := dnssvc.New(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating dns svc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dns = svc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web returns the current web service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.web
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateWeb.
|
||||||
|
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||||
|
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
if prev := m.web; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down web svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.web = websvc.New(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
227
internal/next/dnssvc/dnssvc.go
Normal file
227
internal/next/dnssvc/dnssvc.go
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
// Package dnssvc contains the AdGuard Home DNS service.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Define, if all methods of a *Service should work with a nil
|
||||||
|
// receiver.
|
||||||
|
package dnssvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||||
|
// and replacement of module dnsproxy.
|
||||||
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config is the AdGuard Home DNS service configuration structure.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add timeout for incoming requests.
|
||||||
|
type Config struct {
|
||||||
|
// Addresses are the addresses on which to serve plain DNS queries.
|
||||||
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
|
// Upstreams are the DNS upstreams to use. If not set, upstreams are
|
||||||
|
// created using data from BootstrapServers, UpstreamServers, and
|
||||||
|
// UpstreamTimeout.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Think of a better scheme. Those other three parameters
|
||||||
|
// are here only to make Config work properly.
|
||||||
|
Upstreams []upstream.Upstream
|
||||||
|
|
||||||
|
// BootstrapServers are the addresses for bootstrapping the upstream DNS
|
||||||
|
// server addresses.
|
||||||
|
BootstrapServers []string
|
||||||
|
|
||||||
|
// UpstreamServers are the upstream DNS server addresses to use.
|
||||||
|
UpstreamServers []string
|
||||||
|
|
||||||
|
// UpstreamTimeout is the timeout for upstream requests.
|
||||||
|
UpstreamTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||||
|
// [agh.Service] that does nothing.
|
||||||
|
type Service struct {
|
||||||
|
// running is an atomic boolean value. Keep it the first value in the
|
||||||
|
// struct to ensure atomic alignment. 0 means that the service is not
|
||||||
|
// running, 1 means that it is running.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||||
|
// completely.
|
||||||
|
running uint64
|
||||||
|
|
||||||
|
proxy *proxy.Proxy
|
||||||
|
bootstraps []string
|
||||||
|
upstreams []string
|
||||||
|
upsTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||||
|
// *Service that does nothing. The fields of c must not be modified after
|
||||||
|
// calling New.
|
||||||
|
func New(c *Config) (svc *Service, err error) {
|
||||||
|
if c == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
svc = &Service{
|
||||||
|
bootstraps: c.BootstrapServers,
|
||||||
|
upstreams: c.UpstreamServers,
|
||||||
|
upsTimeout: c.UpstreamTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
var upstreams []upstream.Upstream
|
||||||
|
if len(c.Upstreams) > 0 {
|
||||||
|
upstreams = c.Upstreams
|
||||||
|
} else {
|
||||||
|
upstreams, err = addressesToUpstreams(
|
||||||
|
c.UpstreamServers,
|
||||||
|
c.BootstrapServers,
|
||||||
|
c.UpstreamTimeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting upstreams: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc.proxy = &proxy.Proxy{
|
||||||
|
Config: proxy.Config{
|
||||||
|
UDPListenAddr: udpAddrs(c.Addresses),
|
||||||
|
TCPListenAddr: tcpAddrs(c.Addresses),
|
||||||
|
UpstreamConfig: &proxy.UpstreamConfig{
|
||||||
|
Upstreams: upstreams,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = svc.proxy.Init()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("proxy: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addressesToUpstreams is a wrapper around [upstream.AddressToUpstream]. It
|
||||||
|
// accepts a slice of addresses and other upstream parameters, and returns a
|
||||||
|
// slice of upstreams.
|
||||||
|
func addressesToUpstreams(
|
||||||
|
upsStrs []string,
|
||||||
|
bootstraps []string,
|
||||||
|
timeout time.Duration,
|
||||||
|
) (upstreams []upstream.Upstream, err error) {
|
||||||
|
upstreams = make([]upstream.Upstream, len(upsStrs))
|
||||||
|
for i, upsStr := range upsStrs {
|
||||||
|
upstreams[i], err = upstream.AddressToUpstream(upsStr, &upstream.Options{
|
||||||
|
Bootstrap: bootstraps,
|
||||||
|
Timeout: timeout,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("upstream at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return upstreams, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tcpAddrs converts []netip.AddrPort into []*net.TCPAddr.
|
||||||
|
func tcpAddrs(addrPorts []netip.AddrPort) (tcpAddrs []*net.TCPAddr) {
|
||||||
|
if addrPorts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tcpAddrs = make([]*net.TCPAddr, len(addrPorts))
|
||||||
|
for i, a := range addrPorts {
|
||||||
|
tcpAddrs[i] = net.TCPAddrFromAddrPort(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tcpAddrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// udpAddrs converts []netip.AddrPort into []*net.UDPAddr.
|
||||||
|
func udpAddrs(addrPorts []netip.AddrPort) (udpAddrs []*net.UDPAddr) {
|
||||||
|
if addrPorts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
udpAddrs = make([]*net.UDPAddr, len(addrPorts))
|
||||||
|
for i, a := range addrPorts {
|
||||||
|
udpAddrs[i] = net.UDPAddrFromAddrPort(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
return udpAddrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ agh.Service = (*Service)(nil)
|
||||||
|
|
||||||
|
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||||
|
// After Start exits, all DNS servers have tried to start, but there is no
|
||||||
|
// guarantee that they did. Errors from the servers are written to the log.
|
||||||
|
func (svc *Service) Start() (err error) {
|
||||||
|
if svc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||||
|
// tell when all servers are actually up, so at best this is merely an
|
||||||
|
// assumption.
|
||||||
|
if err != nil {
|
||||||
|
atomic.StoreUint64(&svc.running, 0)
|
||||||
|
} else {
|
||||||
|
atomic.StoreUint64(&svc.running, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return svc.proxy.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||||
|
// nil.
|
||||||
|
func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||||
|
if svc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc.proxy.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config returns the current configuration of the web service. Config must not
|
||||||
|
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||||
|
// addresses, addrs will not return the actual bound ports until Start is
|
||||||
|
// finished.
|
||||||
|
func (svc *Service) Config() (c *Config) {
|
||||||
|
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||||
|
|
||||||
|
var addrs []netip.AddrPort
|
||||||
|
if atomic.LoadUint64(&svc.running) == 1 {
|
||||||
|
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
||||||
|
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||||
|
for i, a := range udpAddrs {
|
||||||
|
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
conf := svc.proxy.Config
|
||||||
|
udpAddrs := conf.UDPListenAddr
|
||||||
|
addrs = make([]netip.AddrPort, len(udpAddrs))
|
||||||
|
for i, a := range udpAddrs {
|
||||||
|
addrs[i] = a.AddrPort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c = &Config{
|
||||||
|
Addresses: addrs,
|
||||||
|
BootstrapServers: svc.bootstraps,
|
||||||
|
UpstreamServers: svc.upstreams,
|
||||||
|
UpstreamTimeout: svc.upsTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
96
internal/next/dnssvc/dnssvc_test.go
Normal file
96
internal/next/dnssvc/dnssvc_test.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package dnssvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/netip"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testutil.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 100 * time.Millisecond
|
||||||
|
|
||||||
|
func TestService(t *testing.T) {
|
||||||
|
const (
|
||||||
|
bootstrapAddr = "bootstrap.example"
|
||||||
|
upstreamAddr = "upstream.example"
|
||||||
|
|
||||||
|
closeErr errors.Error = "closing failed"
|
||||||
|
)
|
||||||
|
|
||||||
|
ups := &aghtest.UpstreamMock{
|
||||||
|
OnAddress: func() (addr string) {
|
||||||
|
return upstreamAddr
|
||||||
|
},
|
||||||
|
OnExchange: func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||||
|
resp = (&dns.Msg{}).SetReply(req)
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
},
|
||||||
|
OnClose: func() (err error) {
|
||||||
|
return closeErr
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &dnssvc.Config{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||||
|
Upstreams: []upstream.Upstream{ups},
|
||||||
|
BootstrapServers: []string{bootstrapAddr},
|
||||||
|
UpstreamServers: []string{upstreamAddr},
|
||||||
|
UpstreamTimeout: testTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := dnssvc.New(c)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = svc.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
gotConf := svc.Config()
|
||||||
|
require.NotNil(t, gotConf)
|
||||||
|
require.Len(t, gotConf.Addresses, 1)
|
||||||
|
|
||||||
|
addr := gotConf.Addresses[0]
|
||||||
|
|
||||||
|
t.Run("dns", func(t *testing.T) {
|
||||||
|
req := &dns.Msg{
|
||||||
|
MsgHdr: dns.MsgHdr{
|
||||||
|
Id: dns.Id(),
|
||||||
|
RecursionDesired: true,
|
||||||
|
},
|
||||||
|
Question: []dns.Question{{
|
||||||
|
Name: "example.com.",
|
||||||
|
Qtype: dns.TypeA,
|
||||||
|
Qclass: dns.ClassINET,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cli := &dns.Client{}
|
||||||
|
resp, _, excErr := cli.ExchangeContext(ctx, req, addr.String())
|
||||||
|
require.NoError(t, excErr)
|
||||||
|
|
||||||
|
assert.NotNil(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = svc.Shutdown(ctx)
|
||||||
|
require.ErrorIs(t, err, closeErr)
|
||||||
|
}
|
||||||
84
internal/next/websvc/dns.go
Normal file
84
internal/next/websvc/dns.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsDNS struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||||
|
// DnsSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIDNSSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsDNS{
|
||||||
|
Addresses: []netip.AddrPort{},
|
||||||
|
BootstrapServers: []string{},
|
||||||
|
UpstreamServers: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &dnssvc.Config{
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
BootstrapServers: req.BootstrapServers,
|
||||||
|
UpstreamServers: req.UpstreamServers,
|
||||||
|
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSvc := svc.confMgr.DNS()
|
||||||
|
err = newSvc.Start()
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
BootstrapServers: newConf.BootstrapServers,
|
||||||
|
UpstreamServers: newConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
}
|
||||||
69
internal/next/websvc/dns_test.go
Normal file
69
internal/next/websvc/dns_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||||
|
BootstrapServers: []string{"1.0.0.1"},
|
||||||
|
UpstreamServers: []string{"1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||||
|
var numStarted uint64
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||||
|
OnStart: func() (err error) {
|
||||||
|
atomic.AddUint64(&numStarted, 1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||||
|
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsDNS,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantDNS.Addresses,
|
||||||
|
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||||
|
"upstream_servers": wantDNS.UpstreamServers,
|
||||||
|
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIDNSSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, uint64(1), numStarted)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
}
|
||||||
110
internal/next/websvc/http.go
Normal file
110
internal/next/websvc/http.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsHTTP struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add wait time.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||||
|
// HttpSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIHTTPSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
ForceHTTPS bool `json:"force_https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsHTTP{}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
SecureAddresses: req.SecureAddresses,
|
||||||
|
Timeout: time.Duration(req.Timeout),
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
SecureAddresses: newConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(newConf.Timeout),
|
||||||
|
ForceHTTPS: newConf.ForceHTTPS,
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelUpd := func() {}
|
||||||
|
updCtx := context.Background()
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||||
|
// finish and thus, this server to shutdown.
|
||||||
|
go func() {
|
||||||
|
defer cancelUpd()
|
||||||
|
|
||||||
|
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||||
|
if updErr != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider better ways to do this.
|
||||||
|
const maxUpdDur = 10 * time.Second
|
||||||
|
updStart := time.Now()
|
||||||
|
var newSvc agh.ServiceWithConfig[*Config]
|
||||||
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
|
if time.Since(updStart) >= maxUpdDur {
|
||||||
|
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("websvc: waiting for new websvc to be configured")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
updErr = newSvc.Start()
|
||||||
|
if updErr != nil {
|
||||||
|
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
63
internal/next/websvc/http_test.go
Normal file
63
internal/next/websvc/http_test.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||||
|
ForceHTTPS: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsHTTP,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantWeb.Addresses,
|
||||||
|
"secure_addresses": wantWeb.SecureAddresses,
|
||||||
|
"timeout": wantWeb.Timeout,
|
||||||
|
"force_https": wantWeb.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantWeb, resp)
|
||||||
|
}
|
||||||
143
internal/next/websvc/json.go
Normal file
143
internal/next/websvc/json.go
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON Utilities
|
||||||
|
|
||||||
|
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||||
|
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||||
|
|
||||||
|
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||||
|
// into JSON according to our API conventions.
|
||||||
|
type JSONDuration time.Duration
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONDuration(0)
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||||
|
// always nil.
|
||||||
|
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||||
|
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("json duration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||||
|
// according to our API conventions.
|
||||||
|
type JSONTime time.Time
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONTime{}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||||
|
// always nil.
|
||||||
|
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||||
|
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("json time is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||||
|
// and logs any errors it encounters. r is used to get additional information
|
||||||
|
// from the request.
|
||||||
|
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||||
|
writeJSONResponse(w, r, v, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||||
|
// TODO(a.garipov): Put some of these to a middleware.
|
||||||
|
h := w.Header()
|
||||||
|
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||||
|
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||||
|
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||||
|
// definition in the OpenAPI specification.
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
// ErrorCode constants.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Expand and document codes.
|
||||||
|
const (
|
||||||
|
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||||
|
ErrorCodeTMP000 = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||||
|
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||||
|
// specification.
|
||||||
|
type HTTPAPIErrorResp struct {
|
||||||
|
Code ErrorCode `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||||
|
Code: ErrorCodeTMP000,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}, http.StatusUnprocessableEntity)
|
||||||
|
}
|
||||||
114
internal/next/websvc/json_test.go
Normal file
114
internal/next/websvc/json_test.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testJSONTime is the JSON time for tests.
|
||||||
|
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||||
|
|
||||||
|
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||||
|
const testJSONTimeStr = "1234567890123.456"
|
||||||
|
|
||||||
|
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
in websvc.JSONTime
|
||||||
|
want []byte
|
||||||
|
}{{
|
||||||
|
name: "unix_zero",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||||
|
want: []byte("0"),
|
||||||
|
}, {
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime{},
|
||||||
|
want: []byte("-6795364578871.345"),
|
||||||
|
}, {
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: testJSONTime,
|
||||||
|
want: []byte(testJSONTimeStr),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.in.MarshalJSON()
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
in := &struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}{
|
||||||
|
A: testJSONTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := json.Marshal(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
want websvc.JSONTime
|
||||||
|
data []byte
|
||||||
|
}{{
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: testJSONTime,
|
||||||
|
data: []byte(testJSONTimeStr),
|
||||||
|
}, {
|
||||||
|
name: "bad",
|
||||||
|
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||||
|
`invalid syntax`,
|
||||||
|
want: websvc.JSONTime{},
|
||||||
|
data: []byte(`{}`),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var got websvc.JSONTime
|
||||||
|
err := got.UnmarshalJSON(tc.data)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
assert.Equal(t, "json time is nil", msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
want := testJSONTime
|
||||||
|
var got struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got.A)
|
||||||
|
})
|
||||||
|
}
|
||||||
16
internal/next/websvc/middleware.go
Normal file
16
internal/next/websvc/middleware.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
// Middlewares
|
||||||
|
|
||||||
|
// jsonMw sets the content type of the response to application/json.
|
||||||
|
func jsonMw(h http.Handler) (wrapped http.HandlerFunc) {
|
||||||
|
f := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
h.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(f)
|
||||||
|
}
|
||||||
11
internal/next/websvc/path.go
Normal file
11
internal/next/websvc/path.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
// Path constants
|
||||||
|
const (
|
||||||
|
PathHealthCheck = "/health-check"
|
||||||
|
|
||||||
|
PathV1SettingsAll = "/api/v1/settings/all"
|
||||||
|
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||||
|
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||||
|
PathV1SystemInfo = "/api/v1/system/info"
|
||||||
|
)
|
||||||
42
internal/next/websvc/settings.go
Normal file
42
internal/next/websvc/settings.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// All Settings Handlers
|
||||||
|
|
||||||
|
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SettingsAll struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
DNS *HTTPAPIDNSSettings `json:"dns"`
|
||||||
|
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsSvc := svc.confMgr.DNS()
|
||||||
|
dnsConf := dnsSvc.Config()
|
||||||
|
|
||||||
|
webSvc := svc.confMgr.Web()
|
||||||
|
httpConf := webSvc.Config()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||||
|
DNS: &HTTPAPIDNSSettings{
|
||||||
|
Addresses: dnsConf.Addresses,
|
||||||
|
BootstrapServers: dnsConf.BootstrapServers,
|
||||||
|
UpstreamServers: dnsConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||||
|
},
|
||||||
|
HTTP: &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: httpConf.Addresses,
|
||||||
|
SecureAddresses: httpConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(httpConf.Timeout),
|
||||||
|
ForceHTTPS: httpConf.ForceHTTPS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
75
internal/next/websvc/settings_test.go
Normal file
75
internal/next/websvc/settings_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||||
|
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||||
|
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
c, err := dnssvc.New(&dnssvc.Config{
|
||||||
|
Addresses: wantDNS.Addresses,
|
||||||
|
UpstreamServers: wantDNS.UpstreamServers,
|
||||||
|
BootstrapServers: wantDNS.BootstrapServers,
|
||||||
|
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: wantWeb.Addresses,
|
||||||
|
SecureAddresses: wantWeb.SecureAddresses,
|
||||||
|
Timeout: time.Duration(wantWeb.Timeout),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
resp := &websvc.RespGetV1SettingsAll{}
|
||||||
|
err := json.Unmarshal(body, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantDNS, resp.DNS)
|
||||||
|
assert.Equal(t, wantWeb, resp.HTTP)
|
||||||
|
}
|
||||||
35
internal/next/websvc/system.go
Normal file
35
internal/next/websvc/system.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
// System Handlers
|
||||||
|
|
||||||
|
// RespGetV1SystemInfo describes the response of the GET /api/v1/system/info
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SystemInfo struct {
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
NewVersion string `json:"new_version,omitempty"`
|
||||||
|
Start JSONTime `json:"start"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||||
|
Arch: runtime.GOARCH,
|
||||||
|
Channel: version.Channel(),
|
||||||
|
OS: runtime.GOOS,
|
||||||
|
// TODO(a.garipov): Fill this when we have an updater.
|
||||||
|
NewVersion: "",
|
||||||
|
Start: JSONTime(svc.start),
|
||||||
|
Version: version.Version(),
|
||||||
|
})
|
||||||
|
}
|
||||||
37
internal/next/websvc/system_test.go
Normal file
37
internal/next/websvc/system_test.go
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SystemInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
resp := &websvc.RespGetV1SystemInfo{}
|
||||||
|
err := json.Unmarshal(body, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider making version.Channel and version.Version
|
||||||
|
// testable and test these better.
|
||||||
|
assert.NotEmpty(t, resp.Channel)
|
||||||
|
|
||||||
|
assert.Equal(t, resp.Arch, runtime.GOARCH)
|
||||||
|
assert.Equal(t, resp.OS, runtime.GOOS)
|
||||||
|
assert.Equal(t, testStart, time.Time(resp.Start))
|
||||||
|
}
|
||||||
31
internal/next/websvc/waitlistener.go
Normal file
31
internal/next/websvc/waitlistener.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait Listener
|
||||||
|
|
||||||
|
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||||
|
// first call to Accept. It is useful in situations where it is important to
|
||||||
|
// catch the precise moment of the first call to Accept, for example when
|
||||||
|
// starting an HTTP server.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to aghnet?
|
||||||
|
type waitListener struct {
|
||||||
|
net.Listener
|
||||||
|
|
||||||
|
firstAcceptWG *sync.WaitGroup
|
||||||
|
firstAcceptOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ net.Listener = (*waitListener)(nil)
|
||||||
|
|
||||||
|
// Accept implements the [net.Listener] interface for *waitListener.
|
||||||
|
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||||
|
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||||
|
|
||||||
|
return l.Listener.Accept()
|
||||||
|
}
|
||||||
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
46
internal/next/websvc/waitlistener_internal_test.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWaitListener_Accept(t *testing.T) {
|
||||||
|
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||||
|
var numAcceptCalls uint32
|
||||||
|
var l net.Listener = &aghtest.Listener{
|
||||||
|
OnAccept: func() (conn net.Conn, err error) {
|
||||||
|
atomic.AddUint32(&numAcceptCalls, 1)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
||||||
|
OnClose: func() (err error) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go aghchan.MustReceive(done, testTimeout)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var wrapper net.Listener = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = wrapper.Accept()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||||
|
}
|
||||||
305
internal/next/websvc/websvc.go
Normal file
305
internal/next/websvc/websvc.go
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
// Package websvc contains the AdGuard Home HTTP API service.
|
||||||
|
//
|
||||||
|
// NOTE: Packages other than cmd must not import this package, as it imports
|
||||||
|
// most other packages.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add tests.
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
httptreemux "github.com/dimfeld/httptreemux/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConfigManager is the configuration manager interface.
|
||||||
|
type ConfigManager interface {
|
||||||
|
DNS() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||||
|
Web() (svc agh.ServiceWithConfig[*Config])
|
||||||
|
|
||||||
|
UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
|
UpdateWeb(ctx context.Context, c *Config) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
|
type Config struct {
|
||||||
|
// ConfigManager is used to show information about services as well as
|
||||||
|
// dynamically reconfigure them.
|
||||||
|
ConfigManager ConfigManager
|
||||||
|
|
||||||
|
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||||
|
// SecureAddresses must not be empty.
|
||||||
|
TLS *tls.Config
|
||||||
|
|
||||||
|
// Start is the time of start of AdGuard Home.
|
||||||
|
Start time.Time
|
||||||
|
|
||||||
|
// Addresses are the addresses on which to serve the plain HTTP API.
|
||||||
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
|
// SecureAddresses are the addresses on which to serve the HTTPS API. If
|
||||||
|
// SecureAddresses is not empty, TLS must not be nil.
|
||||||
|
SecureAddresses []netip.AddrPort
|
||||||
|
|
||||||
|
// Timeout is the timeout for all server operations.
|
||||||
|
Timeout time.Duration
|
||||||
|
|
||||||
|
// ForceHTTPS tells if all requests to Addresses should be redirected to a
|
||||||
|
// secure address instead.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use; define rules, which address to redirect to.
|
||||||
|
ForceHTTPS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||||
|
// [agh.Service] that does nothing.
|
||||||
|
type Service struct {
|
||||||
|
confMgr ConfigManager
|
||||||
|
tls *tls.Config
|
||||||
|
start time.Time
|
||||||
|
servers []*http.Server
|
||||||
|
timeout time.Duration
|
||||||
|
forceHTTPS bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||||
|
// *Service that does nothing. The fields of c must not be modified after
|
||||||
|
// calling New.
|
||||||
|
func New(c *Config) (svc *Service) {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
svc = &Service{
|
||||||
|
confMgr: c.ConfigManager,
|
||||||
|
tls: c.TLS,
|
||||||
|
start: c.Start,
|
||||||
|
timeout: c.Timeout,
|
||||||
|
forceHTTPS: c.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := newMux(svc)
|
||||||
|
|
||||||
|
for _, a := range c.Addresses {
|
||||||
|
addr := a.String()
|
||||||
|
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||||
|
svc.servers = append(svc.servers, &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
ErrorLog: errLog,
|
||||||
|
ReadTimeout: c.Timeout,
|
||||||
|
WriteTimeout: c.Timeout,
|
||||||
|
IdleTimeout: c.Timeout,
|
||||||
|
ReadHeaderTimeout: c.Timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, a := range c.SecureAddresses {
|
||||||
|
addr := a.String()
|
||||||
|
errLog := log.StdLog("websvc: https: "+addr, log.ERROR)
|
||||||
|
svc.servers = append(svc.servers, &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
|
TLSConfig: c.TLS,
|
||||||
|
ErrorLog: errLog,
|
||||||
|
ReadTimeout: c.Timeout,
|
||||||
|
WriteTimeout: c.Timeout,
|
||||||
|
IdleTimeout: c.Timeout,
|
||||||
|
ReadHeaderTimeout: c.Timeout,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
// newMux returns a new HTTP request multiplexor for the AdGuard Home web
|
||||||
|
// service.
|
||||||
|
func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||||
|
mux = httptreemux.NewContextMux()
|
||||||
|
|
||||||
|
routes := []struct {
|
||||||
|
handler http.HandlerFunc
|
||||||
|
method string
|
||||||
|
path string
|
||||||
|
isJSON bool
|
||||||
|
}{{
|
||||||
|
handler: svc.handleGetHealthCheck,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathHealthCheck,
|
||||||
|
isJSON: false,
|
||||||
|
}, {
|
||||||
|
handler: svc.handleGetSettingsAll,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathV1SettingsAll,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: svc.handlePatchSettingsDNS,
|
||||||
|
method: http.MethodPatch,
|
||||||
|
path: PathV1SettingsDNS,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: svc.handlePatchSettingsHTTP,
|
||||||
|
method: http.MethodPatch,
|
||||||
|
path: PathV1SettingsHTTP,
|
||||||
|
isJSON: true,
|
||||||
|
}, {
|
||||||
|
handler: svc.handleGetV1SystemInfo,
|
||||||
|
method: http.MethodGet,
|
||||||
|
path: PathV1SystemInfo,
|
||||||
|
isJSON: true,
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, r := range routes {
|
||||||
|
if r.isJSON {
|
||||||
|
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||||
|
} else {
|
||||||
|
mux.Handle(r.method, r.path, r.handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||||
|
// must not be called simultaneously with Start. If svc was initialized with
|
||||||
|
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||||
|
// finished.
|
||||||
|
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||||
|
for _, srv := range svc.servers {
|
||||||
|
addrPort, err := netip.ParseAddrPort(srv.Addr)
|
||||||
|
if err != nil {
|
||||||
|
// Technically shouldn't happen, since all servers must have a valid
|
||||||
|
// address.
|
||||||
|
panic(fmt.Errorf("websvc: server %q: bad address: %w", srv.Addr, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// srv.Serve will set TLSConfig to an almost empty value, so, instead of
|
||||||
|
// relying only on the nilness of TLSConfig, check the length of the
|
||||||
|
// certificates field as well.
|
||||||
|
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
|
||||||
|
addrs = append(addrs, addrPort)
|
||||||
|
} else {
|
||||||
|
secureAddrs = append(secureAddrs, addrPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return addrs, secureAddrs
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||||
|
func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
_, _ = io.WriteString(w, "OK")
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ agh.Service = (*Service)(nil)
|
||||||
|
|
||||||
|
// Start implements the [agh.Service] interface for *Service. svc may be nil.
|
||||||
|
// After Start exits, all HTTP servers have tried to start, possibly failing and
|
||||||
|
// writing error messages to the log.
|
||||||
|
func (svc *Service) Start() (err error) {
|
||||||
|
if svc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(svc.servers))
|
||||||
|
for _, srv := range svc.servers {
|
||||||
|
go serve(srv, wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serve starts and runs srv and writes all errors into its log.
|
||||||
|
func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||||
|
addr := srv.Addr
|
||||||
|
defer log.OnPanic(addr)
|
||||||
|
|
||||||
|
var proto string
|
||||||
|
var l net.Listener
|
||||||
|
var err error
|
||||||
|
if srv.TLSConfig == nil {
|
||||||
|
proto = "http"
|
||||||
|
l, err = net.Listen("tcp", addr)
|
||||||
|
} else {
|
||||||
|
proto = "https"
|
||||||
|
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
srv.ErrorLog.Printf("starting srv %s: binding: %s", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the server's address in case the address had the port zero, which
|
||||||
|
// would mean that a random available port was automatically chosen.
|
||||||
|
srv.Addr = l.Addr().String()
|
||||||
|
|
||||||
|
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||||
|
|
||||||
|
l = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = srv.Serve(l)
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
srv.ErrorLog.Printf("starting srv %s: %s", addr, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown implements the [agh.Service] interface for *Service. svc may be
|
||||||
|
// nil.
|
||||||
|
func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||||
|
if svc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for _, srv := range svc.servers {
|
||||||
|
serr := srv.Shutdown(ctx)
|
||||||
|
if serr != nil {
|
||||||
|
errs = append(errs, fmt.Errorf("shutting down srv %s: %w", srv.Addr, serr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) > 0 {
|
||||||
|
return errors.List("shutting down", errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config returns the current configuration of the web service. Config must not
|
||||||
|
// be called simultaneously with Start. If svc was initialized with ":0"
|
||||||
|
// addresses, addrs will not return the actual bound ports until Start is
|
||||||
|
// finished.
|
||||||
|
func (svc *Service) Config() (c *Config) {
|
||||||
|
c = &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
// Leave Addresses and SecureAddresses empty and get the actual
|
||||||
|
// addresses that include the :0 ones later.
|
||||||
|
Start: svc.start,
|
||||||
|
Timeout: svc.timeout,
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Addresses, c.SecureAddresses = svc.addrs()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
6
internal/next/websvc/websvc_internal_test.go
Normal file
6
internal/next/websvc/websvc_internal_test.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package websvc
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
187
internal/next/websvc/websvc_test.go
Normal file
187
internal/next/websvc/websvc_test.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
testutil.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
||||||
|
|
||||||
|
// testStart is the server start value for tests.
|
||||||
|
var testStart = time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ websvc.ConfigManager = (*configManager)(nil)
|
||||||
|
|
||||||
|
// configManager is a [websvc.ConfigManager] for tests.
|
||||||
|
type configManager struct {
|
||||||
|
onDNS func() (svc agh.ServiceWithConfig[*dnssvc.Config])
|
||||||
|
onWeb func() (svc agh.ServiceWithConfig[*websvc.Config])
|
||||||
|
|
||||||
|
onUpdateDNS func(ctx context.Context, c *dnssvc.Config) (err error)
|
||||||
|
onUpdateWeb func(ctx context.Context, c *websvc.Config) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) DNS() (svc agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
return m.onDNS()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) Web() (svc agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return m.onWeb()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNS implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
return m.onUpdateDNS(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWeb implements the [websvc.ConfigManager] interface for *configManager.
|
||||||
|
func (m *configManager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
return m.onUpdateWeb(ctx, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConfigManager returns a *configManager all methods of which panic.
|
||||||
|
func newConfigManager() (m *configManager) {
|
||||||
|
return &configManager{
|
||||||
|
onDNS: func() (svc agh.ServiceWithConfig[*dnssvc.Config]) { panic("not implemented") },
|
||||||
|
onWeb: func() (svc agh.ServiceWithConfig[*websvc.Config]) { panic("not implemented") },
|
||||||
|
onUpdateDNS: func(_ context.Context, _ *dnssvc.Config) (err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
onUpdateWeb: func(_ context.Context, _ *websvc.Config) (err error) {
|
||||||
|
panic("not implemented")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTestServer creates and starts a new web service instance as well as its
|
||||||
|
// sole address. It also registers a cleanup procedure, which shuts the
|
||||||
|
// instance down.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use svc or remove it.
|
||||||
|
func newTestServer(
|
||||||
|
t testing.TB,
|
||||||
|
confMgr websvc.ConfigManager,
|
||||||
|
) (svc *websvc.Service, addr netip.AddrPort) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
c := &websvc.Config{
|
||||||
|
ConfigManager: confMgr,
|
||||||
|
TLS: nil,
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:0")},
|
||||||
|
SecureAddresses: nil,
|
||||||
|
Timeout: testTimeout,
|
||||||
|
Start: testStart,
|
||||||
|
ForceHTTPS: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
svc = websvc.New(c)
|
||||||
|
|
||||||
|
err := svc.Start()
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
|
||||||
|
t.Cleanup(cancel)
|
||||||
|
|
||||||
|
err = svc.Shutdown(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
c = svc.Config()
|
||||||
|
require.NotNil(t, c)
|
||||||
|
require.Len(t, c.Addresses, 1)
|
||||||
|
|
||||||
|
return svc, c.Addresses[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// jobj is a utility alias for JSON objects.
|
||||||
|
type jobj map[string]any
|
||||||
|
|
||||||
|
// httpGet is a helper that performs an HTTP GET request and returns the body of
|
||||||
|
// the response as well as checks that the status code is correct.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add helpers for other methods.
|
||||||
|
func httpGet(t testing.TB, u *url.URL, wantCode int) (body []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||||
|
require.NoErrorf(t, err, "creating req")
|
||||||
|
|
||||||
|
httpCli := &http.Client{
|
||||||
|
Timeout: testTimeout,
|
||||||
|
}
|
||||||
|
resp, err := httpCli.Do(req)
|
||||||
|
require.NoErrorf(t, err, "performing req")
|
||||||
|
require.Equal(t, wantCode, resp.StatusCode)
|
||||||
|
|
||||||
|
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoErrorf(t, err, "reading body")
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpPatch is a helper that performs an HTTP PATCH request with JSON-encoded
|
||||||
|
// reqBody as the request body and returns the body of the response as well as
|
||||||
|
// checks that the status code is correct.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add helpers for other methods.
|
||||||
|
func httpPatch(t testing.TB, u *url.URL, reqBody any, wantCode int) (body []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
b, err := json.Marshal(reqBody)
|
||||||
|
require.NoErrorf(t, err, "marshaling reqBody")
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPatch, u.String(), bytes.NewReader(b))
|
||||||
|
require.NoErrorf(t, err, "creating req")
|
||||||
|
|
||||||
|
httpCli := &http.Client{
|
||||||
|
Timeout: testTimeout,
|
||||||
|
}
|
||||||
|
resp, err := httpCli.Do(req)
|
||||||
|
require.NoErrorf(t, err, "performing req")
|
||||||
|
require.Equal(t, wantCode, resp.StatusCode)
|
||||||
|
|
||||||
|
testutil.CleanupAndRequireSuccess(t, resp.Body.Close)
|
||||||
|
|
||||||
|
body, err = io.ReadAll(resp.Body)
|
||||||
|
require.NoErrorf(t, err, "reading body")
|
||||||
|
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestService_Start_getHealthCheck(t *testing.T) {
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathHealthCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte("OK"), body)
|
||||||
|
}
|
||||||
@@ -180,7 +180,7 @@ func withRecovered(orig *error) {
|
|||||||
// type check
|
// type check
|
||||||
var _ Interface = (*StatsCtx)(nil)
|
var _ Interface = (*StatsCtx)(nil)
|
||||||
|
|
||||||
// Start implements the Interface interface for *StatsCtx.
|
// Start implements the [Interface] interface for *StatsCtx.
|
||||||
func (s *StatsCtx) Start() {
|
func (s *StatsCtx) Start() {
|
||||||
s.initWeb()
|
s.initWeb()
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ func (u *Updater) VersionInfo(forceRecheck bool) (vi VersionInfo, err error) {
|
|||||||
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
return VersionInfo{}, fmt.Errorf("updater: HTTP GET %s: %w", vcu, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.prevCheckTime = time.Now()
|
u.prevCheckTime = now
|
||||||
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
u.prevCheckResult, u.prevCheckError = u.parseVersionResponse(body)
|
||||||
|
|
||||||
return u.prevCheckResult, u.prevCheckError
|
return u.prevCheckResult, u.prevCheckError
|
||||||
@@ -92,7 +92,11 @@ func (u *Updater) parseVersionResponse(data []byte) (VersionInfo, error) {
|
|||||||
info.AnnouncementURL = versionJSON["announcement_url"]
|
info.AnnouncementURL = versionJSON["announcement_url"]
|
||||||
|
|
||||||
packageURL, ok := u.downloadURL(versionJSON)
|
packageURL, ok := u.downloadURL(versionJSON)
|
||||||
info.CanAutoUpdate = aghalg.BoolToNullBool(ok && info.NewVersion != u.version)
|
if !ok {
|
||||||
|
return info, fmt.Errorf("version.json: packageURL not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
info.CanAutoUpdate = aghalg.BoolToNullBool(info.NewVersion != u.version)
|
||||||
|
|
||||||
u.newVersion = info.NewVersion
|
u.newVersion = info.NewVersion
|
||||||
u.packageURL = packageURL
|
u.packageURL = packageURL
|
||||||
|
|||||||
@@ -104,49 +104,58 @@ func NewUpdater(conf *Config) *Updater {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update performs the auto-update.
|
// Update performs the auto-update. It returns an error if the update failed.
|
||||||
func (u *Updater) Update() (err error) {
|
// If firstRun is true, it assumes the configuration file doesn't exist.
|
||||||
|
func (u *Updater) Update(firstRun bool) (err error) {
|
||||||
u.mu.Lock()
|
u.mu.Lock()
|
||||||
defer u.mu.Unlock()
|
defer u.mu.Unlock()
|
||||||
|
|
||||||
log.Info("updater: updating")
|
log.Info("updater: updating")
|
||||||
defer func() { log.Info("updater: finished; errors: %v", err) }()
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.Error("updater: failed: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Info("updater: finished")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("getting executable path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.prepare(execPath)
|
err = u.prepare(execPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("preparing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer u.clean()
|
defer u.clean()
|
||||||
|
|
||||||
err = u.downloadPackageFile(u.packageURL, u.packageName)
|
err = u.downloadPackageFile()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("downloading package file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.unpack()
|
err = u.unpack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("unpacking: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.check()
|
if !firstRun {
|
||||||
if err != nil {
|
err = u.check()
|
||||||
return err
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking config: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.backup()
|
err = u.backup(firstRun)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("making backup: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = u.replace()
|
err = u.replace()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("replacing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -174,7 +183,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
|||||||
|
|
||||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||||
if pkgNameOnly == "" {
|
if pkgNameOnly == "" {
|
||||||
return fmt.Errorf("invalid PackageURL")
|
return fmt.Errorf("invalid PackageURL: %q", u.packageURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
|
u.packageName = filepath.Join(u.updateDir, pkgNameOnly)
|
||||||
@@ -204,6 +213,7 @@ func (u *Updater) prepare(exePath string) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unpack extracts the files from the downloaded archive.
|
||||||
func (u *Updater) unpack() error {
|
func (u *Updater) unpack() error {
|
||||||
var err error
|
var err error
|
||||||
_, pkgNameOnly := filepath.Split(u.packageURL)
|
_, pkgNameOnly := filepath.Split(u.packageURL)
|
||||||
@@ -228,38 +238,48 @@ func (u *Updater) unpack() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check returns an error if the configuration file couldn't be used with the
|
||||||
|
// version of AdGuard Home just downloaded.
|
||||||
func (u *Updater) check() error {
|
func (u *Updater) check() error {
|
||||||
log.Debug("updater: checking configuration")
|
log.Debug("updater: checking configuration")
|
||||||
|
|
||||||
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
|
err := copyFile(u.confName, filepath.Join(u.updateDir, "AdGuardHome.yaml"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copyFile() failed: %w", err)
|
return fmt.Errorf("copyFile() failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command(u.updateExeName, "--check-config")
|
cmd := exec.Command(u.updateExeName, "--check-config")
|
||||||
err = cmd.Run()
|
err = cmd.Run()
|
||||||
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
if err != nil || cmd.ProcessState.ExitCode() != 0 {
|
||||||
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
return fmt.Errorf("exec.Command(): %s %d", err, cmd.ProcessState.ExitCode())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Updater) backup() error {
|
// backup makes a backup of the current configuration and supporting files. It
|
||||||
|
// ignores the configuration file if firstRun is true.
|
||||||
|
func (u *Updater) backup(firstRun bool) (err error) {
|
||||||
log.Debug("updater: backing up current configuration")
|
log.Debug("updater: backing up current configuration")
|
||||||
_ = os.Mkdir(u.backupDir, 0o755)
|
_ = os.Mkdir(u.backupDir, 0o755)
|
||||||
err := copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
if !firstRun {
|
||||||
if err != nil {
|
err = copyFile(u.confName, filepath.Join(u.backupDir, "AdGuardHome.yaml"))
|
||||||
return fmt.Errorf("copyFile() failed: %w", err)
|
if err != nil {
|
||||||
|
return fmt.Errorf("copyFile() failed: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wd := u.workDir
|
wd := u.workDir
|
||||||
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
err = copySupportingFiles(u.unpackedFiles, wd, u.backupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s",
|
return fmt.Errorf("copySupportingFiles(%s, %s) failed: %s", wd, u.backupDir, err)
|
||||||
wd, u.backupDir, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// replace moves the current executable with the updated one and also copies the
|
||||||
|
// supporting files.
|
||||||
func (u *Updater) replace() error {
|
func (u *Updater) replace() error {
|
||||||
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
err := copySupportingFiles(u.unpackedFiles, u.updateDir, u.workDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -287,6 +307,7 @@ func (u *Updater) replace() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clean removes the temporary directory itself and all it's contents.
|
||||||
func (u *Updater) clean() {
|
func (u *Updater) clean() {
|
||||||
_ = os.RemoveAll(u.updateDir)
|
_ = os.RemoveAll(u.updateDir)
|
||||||
}
|
}
|
||||||
@@ -297,9 +318,9 @@ func (u *Updater) clean() {
|
|||||||
const MaxPackageFileSize = 32 * 1024 * 1024
|
const MaxPackageFileSize = 32 * 1024 * 1024
|
||||||
|
|
||||||
// Download package file and save it to disk
|
// Download package file and save it to disk
|
||||||
func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
func (u *Updater) downloadPackageFile() (err error) {
|
||||||
var resp *http.Response
|
var resp *http.Response
|
||||||
resp, err = u.client.Get(url)
|
resp, err = u.client.Get(u.packageURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("http request failed: %w", err)
|
return fmt.Errorf("http request failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -321,7 +342,7 @@ func (u *Updater) downloadPackageFile(url, filename string) (err error) {
|
|||||||
_ = os.Mkdir(u.updateDir, 0o755)
|
_ = os.Mkdir(u.updateDir, 0o755)
|
||||||
|
|
||||||
log.Debug("updater: saving package to file")
|
log.Debug("updater: saving package to file")
|
||||||
err = os.WriteFile(filename, body, 0o644)
|
err = os.WriteFile(u.packageName, body, 0o644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("os.WriteFile() failed: %w", err)
|
return fmt.Errorf("os.WriteFile() failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,10 +136,10 @@ func TestUpdate(t *testing.T) {
|
|||||||
u.packageURL = fakeURL.String()
|
u.packageURL = fakeURL.String()
|
||||||
|
|
||||||
require.NoError(t, u.prepare(exePath))
|
require.NoError(t, u.prepare(exePath))
|
||||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
require.NoError(t, u.downloadPackageFile())
|
||||||
require.NoError(t, u.unpack())
|
require.NoError(t, u.unpack())
|
||||||
// require.NoError(t, u.check())
|
// require.NoError(t, u.check())
|
||||||
require.NoError(t, u.backup())
|
require.NoError(t, u.backup(false))
|
||||||
require.NoError(t, u.replace())
|
require.NoError(t, u.replace())
|
||||||
|
|
||||||
u.clean()
|
u.clean()
|
||||||
@@ -215,10 +215,10 @@ func TestUpdateWindows(t *testing.T) {
|
|||||||
u.packageURL = fakeURL.String()
|
u.packageURL = fakeURL.String()
|
||||||
|
|
||||||
require.NoError(t, u.prepare(exePath))
|
require.NoError(t, u.prepare(exePath))
|
||||||
require.NoError(t, u.downloadPackageFile(u.packageURL, u.packageName))
|
require.NoError(t, u.downloadPackageFile())
|
||||||
require.NoError(t, u.unpack())
|
require.NoError(t, u.unpack())
|
||||||
// assert.Nil(t, u.check())
|
// assert.Nil(t, u.check())
|
||||||
require.NoError(t, u.backup())
|
require.NoError(t, u.backup(false))
|
||||||
require.NoError(t, u.replace())
|
require.NoError(t, u.replace())
|
||||||
|
|
||||||
u.clean()
|
u.clean()
|
||||||
|
|||||||
3
main.go
3
main.go
@@ -1,3 +1,6 @@
|
|||||||
|
//go:build !next
|
||||||
|
// +build !next
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
21
main_next.go
Normal file
21
main_next.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
//go:build next
|
||||||
|
// +build next
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Embed the prebuilt client here since we strive to keep .go files inside the
|
||||||
|
// internal directory and the embed package is unable to embed files located
|
||||||
|
// outside of the same or underlying directory.
|
||||||
|
|
||||||
|
//go:embed build2
|
||||||
|
var clientBuildFS embed.FS
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Main(clientBuildFS)
|
||||||
|
}
|
||||||
5041
openapi/v1.yaml
Normal file
5041
openapi/v1.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -123,4 +123,14 @@ CGO_ENABLED="$cgo_enabled"
|
|||||||
GO111MODULE='on'
|
GO111MODULE='on'
|
||||||
export CGO_ENABLED GO111MODULE
|
export CGO_ENABLED GO111MODULE
|
||||||
|
|
||||||
"$go" build --ldflags "$ldflags" "$race_flags" --trimpath "$o_flags" "$v_flags" "$x_flags"
|
# Build the new binary if requested.
|
||||||
|
if [ "${NEXTAPI:-0}" -eq '0' ]
|
||||||
|
then
|
||||||
|
tags_flags='--tags='
|
||||||
|
else
|
||||||
|
tags_flags='--tags=next'
|
||||||
|
fi
|
||||||
|
readonly tags_flags
|
||||||
|
|
||||||
|
"$go" build --ldflags "$ldflags" "$race_flags" "$tags_flags" --trimpath "$o_flags" "$v_flags"\
|
||||||
|
"$x_flags"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user