Compare commits
198 Commits
v0.108.0-b
...
v0.107.26
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a21558f418 | ||
|
|
4f928be393 | ||
|
|
f543b47261 | ||
|
|
66b831072c | ||
|
|
80eb339896 | ||
|
|
c69639c013 | ||
|
|
5f6fbe8e08 | ||
|
|
b40bbf0260 | ||
|
|
a11c8e91ab | ||
|
|
618d0e596c | ||
|
|
fde9ea5cb1 | ||
|
|
03d9803238 | ||
|
|
bd64b8b014 | ||
|
|
67fe064fcf | ||
|
|
471668d19a | ||
|
|
42762dfe54 | ||
|
|
c9314610d4 | ||
|
|
16755c37d8 | ||
|
|
73fcbd6ea2 | ||
|
|
30244f361f | ||
|
|
083991fb21 | ||
|
|
e3200d5046 | ||
|
|
21f6ed36fe | ||
|
|
77d04d44eb | ||
|
|
b34d119255 | ||
|
|
63bd71a10c | ||
|
|
faf2b32389 | ||
|
|
d23da1b757 | ||
|
|
beb8e36eee | ||
|
|
fe70161c01 | ||
|
|
39fa4b1f8e | ||
|
|
c7a8883201 | ||
|
|
3fd467413c | ||
|
|
9728dd856f | ||
|
|
ecadf78d60 | ||
|
|
eba4612d72 | ||
|
|
9200163f85 | ||
|
|
3c17853344 | ||
|
|
993a3fc42c | ||
|
|
7bb9b2416b | ||
|
|
2de321ce24 | ||
|
|
30b2b85ff1 | ||
|
|
6ea4788f56 | ||
|
|
3c52a021b9 | ||
|
|
0ceea9af5f | ||
|
|
39b404be19 | ||
|
|
56dc3eab02 | ||
|
|
554a38eeb1 | ||
|
|
c8d3afe869 | ||
|
|
44222c604c | ||
|
|
cbf221585e | ||
|
|
48322f6d0d | ||
|
|
d5a213c639 | ||
|
|
8166c4bc33 | ||
|
|
133cd9ef6b | ||
|
|
11146f73ed | ||
|
|
1beb18db47 | ||
|
|
f7bc2273a7 | ||
|
|
d1e735a003 | ||
|
|
af4ff5c748 | ||
|
|
fc951c1226 | ||
|
|
f81fd42472 | ||
|
|
1029ea5966 | ||
|
|
c0abdb4bc7 | ||
|
|
6681178ad3 | ||
|
|
e73605c4c5 | ||
|
|
c7017d49aa | ||
|
|
191d3bde49 | ||
|
|
18876a8e5c | ||
|
|
aa4a0d9880 | ||
|
|
d03d731d65 | ||
|
|
33b58a42fe | ||
|
|
2e9e708647 | ||
|
|
8ad22841ab | ||
|
|
32cf02264c | ||
|
|
0e8445b38f | ||
|
|
cb27ecd6c0 | ||
|
|
535220b3df | ||
|
|
7b9cfa94f8 | ||
|
|
b3f2e88e9c | ||
|
|
aa7a8d45e4 | ||
|
|
49cdef3d6a | ||
|
|
fecd146552 | ||
|
|
b01efd8c98 | ||
|
|
bd4dfb261c | ||
|
|
e754e4d2f6 | ||
|
|
b220e35c99 | ||
|
|
4f5131f423 | ||
|
|
dcb043df5f | ||
|
|
86e5756262 | ||
|
|
ba0cf5739b | ||
|
|
c4a13b92d2 | ||
|
|
723279121a | ||
|
|
3ad7649f7d | ||
|
|
2898a49d86 | ||
|
|
1547f9d35e | ||
|
|
adadd55c42 | ||
|
|
33b0225aa4 | ||
|
|
97d4058d80 | ||
|
|
86207e719d | ||
|
|
113f94ff46 | ||
|
|
5673deb391 | ||
|
|
3548a393ed | ||
|
|
254515f274 | ||
|
|
bccbecc6ea | ||
|
|
66f53803af | ||
|
|
faef005ce7 | ||
|
|
941cd2a562 | ||
|
|
6a4a9a0239 | ||
|
|
b9dbe6f1b6 | ||
|
|
7fec111ef8 | ||
|
|
5e1bd99718 | ||
|
|
9d75f72ceb | ||
|
|
d98d96db1a | ||
|
|
6a0ef2df15 | ||
|
|
75c2eb4c8a | ||
|
|
d021a67d66 | ||
|
|
4ed97cab12 | ||
|
|
a38742eed7 | ||
|
|
5efa95ed26 | ||
|
|
04db7db607 | ||
|
|
d17c6c6bb3 | ||
|
|
b2052f2ef1 | ||
|
|
cddcf852c2 | ||
|
|
1def426b45 | ||
|
|
b114fd5279 | ||
|
|
d27c3284f6 | ||
|
|
ba24a26b53 | ||
|
|
3e6678b6b4 | ||
|
|
83fd6f9782 | ||
|
|
52bc1b3f10 | ||
|
|
dd2153b7ac | ||
|
|
dd96a34861 | ||
|
|
daf26ee25a | ||
|
|
7e140eaaac | ||
|
|
d07a712988 | ||
|
|
95863288bf | ||
|
|
ea12be658b | ||
|
|
faa7c9aae5 | ||
|
|
e3653e8c25 | ||
|
|
b40cb24822 | ||
|
|
74004c1aa0 | ||
|
|
3e240741f1 | ||
|
|
6cfdbef1a5 | ||
|
|
d9bde6425b | ||
|
|
e2ae9e1591 | ||
|
|
5ebcbfa9ad | ||
|
|
e276bd7a31 | ||
|
|
659b2529bf | ||
|
|
97b3ed43ab | ||
|
|
767d6d3f28 | ||
|
|
31fc9bfc52 | ||
|
|
3f06b02409 | ||
|
|
5bf958ec6b | ||
|
|
959d9ff9a0 | ||
|
|
4813b4de25 | ||
|
|
119100924c | ||
|
|
bd584de4ee | ||
|
|
ede85ab2f2 | ||
|
|
12c20288e4 | ||
|
|
5bbbf89c10 | ||
|
|
d55393ecd5 | ||
|
|
2b5927306f | ||
|
|
4f016b6ed7 | ||
|
|
3a2a6d10ec | ||
|
|
2491426b09 | ||
|
|
5ebdd1390e | ||
|
|
b7f0247575 | ||
|
|
e28186a28a | ||
|
|
de1a7ce48f | ||
|
|
48480fb33b | ||
|
|
f41332fe6b | ||
|
|
1f8b340b8f | ||
|
|
fdaf1d09d3 | ||
|
|
b9682c4f10 | ||
|
|
69dcb4effd | ||
|
|
d50fd0ba91 | ||
|
|
c2c7b4c731 | ||
|
|
952d5f3a3d | ||
|
|
3f126c9ec9 | ||
|
|
0be58ef918 | ||
|
|
8f9053e2fc | ||
|
|
68452e5330 | ||
|
|
2eacc46eaa | ||
|
|
74dcc91ea7 | ||
|
|
dd7bf61323 | ||
|
|
2819d6cace | ||
|
|
75355a6883 | ||
|
|
e9c007d56b | ||
|
|
84c9085516 | ||
|
|
9f36e57c1e | ||
|
|
7528699fc2 | ||
|
|
d280151c18 | ||
|
|
b44c755d25 | ||
|
|
e4078e87a1 | ||
|
|
be36204756 | ||
|
|
b5409d6d00 | ||
|
|
f3d6bce03e |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'build'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.19.6'
|
||||
'GO_VERSION': '1.19.7'
|
||||
'NODE_VERSION': '14'
|
||||
|
||||
'on':
|
||||
|
||||
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
@@ -1,7 +1,7 @@
|
||||
'name': 'lint'
|
||||
|
||||
'env':
|
||||
'GO_VERSION': '1.19.6'
|
||||
'GO_VERSION': '1.19.7'
|
||||
|
||||
'on':
|
||||
'push':
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -14,22 +14,68 @@ and this project adheres to
|
||||
<!--
|
||||
## [v0.108.0] - TBA
|
||||
|
||||
## [v0.107.26] - 2023-03-09 (APPROX.)
|
||||
## [v0.107.27] - 2023-03-29 (APPROX.)
|
||||
|
||||
See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
|
||||
See also the [v0.107.27 GitHub milestone][ms-v0.107.27].
|
||||
|
||||
[ms-v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/milestone/62?closed=1
|
||||
[ms-v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/milestone/63?closed=1
|
||||
|
||||
NOTE: Add new changes BELOW THIS COMMENT.
|
||||
-->
|
||||
|
||||
|
||||
|
||||
## [v0.107.26] - 2023-03-09
|
||||
|
||||
See also the [v0.107.26 GitHub milestone][ms-v0.107.26].
|
||||
|
||||
### Security
|
||||
|
||||
- Go version has been updated to prevent the possibility of exploiting the
|
||||
CVE-2023-24532 Go vulnerability fixed in [Go 1.19.7][go-1.19.7].
|
||||
|
||||
### Added
|
||||
|
||||
- The ability to set custom IP for EDNS Client Subnet by using the new
|
||||
`dns.edns_client_subnet.use_custom` and `dns.edns_client_subnet.custom_ip`
|
||||
fields ([#1472]). The UI changes are coming in the upcoming releases.
|
||||
- The ability to use `dnstype` rules in the disallowed domains list ([#5468]).
|
||||
This allows dropping requests based on their question types.
|
||||
|
||||
### Changed
|
||||
|
||||
#### Configuration Changes
|
||||
|
||||
In this release, the schema version has changed from 16 to 17.
|
||||
|
||||
- Property `edns_client_subnet`, which in schema versions 16 and earlier used
|
||||
to be a part of the `dns` object, is now part of the `dns.edns_client_subnet`
|
||||
object:
|
||||
|
||||
```yaml
|
||||
# BEFORE:
|
||||
'dns':
|
||||
# …
|
||||
'edns_client_subnet': false
|
||||
|
||||
# AFTER:
|
||||
'dns':
|
||||
# …
|
||||
'edns_client_subnet':
|
||||
'enabled': false
|
||||
'use_custom': false
|
||||
'custom_ip': ''
|
||||
```
|
||||
|
||||
To rollback this change, move the value of `dns.edns_client_subnet.enabled`
|
||||
into the `dns.edns_client_subnet`, remove the fields
|
||||
`dns.edns_client_subnet.enabled`, `dns.edns_client_subnet.use_custom`,
|
||||
`dns.edns_client_subnet.custom_ip`, and change the `schema_version` back to
|
||||
`16`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Obsolete value of the Interface MTU DHCP option is now omitted ([#5281]).
|
||||
- Various dark theme bugs ([#5439], [#5441], [#5442], [#5515]).
|
||||
- Automatic update on MIPS64 and little-endian 32-bit MIPS architectures
|
||||
([#5270], [#5373]).
|
||||
@@ -37,8 +83,10 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
been relaxed to meet those from [RFC 3696][rfc3696] ([#4884]).
|
||||
- Failing service installation via script on FreeBSD ([#5431]).
|
||||
|
||||
[#1472]: https://github.com/AdguardTeam/AdGuardHome/issues/1472
|
||||
[#4884]: https://github.com/AdguardTeam/AdGuardHome/issues/4884
|
||||
[#5270]: https://github.com/AdguardTeam/AdGuardHome/issues/5270
|
||||
[#5281]: https://github.com/AdguardTeam/AdGuardHome/issues/5281
|
||||
[#5373]: https://github.com/AdguardTeam/AdGuardHome/issues/5373
|
||||
[#5431]: https://github.com/AdguardTeam/AdGuardHome/issues/5431
|
||||
[#5439]: https://github.com/AdguardTeam/AdGuardHome/issues/5439
|
||||
@@ -47,7 +95,9 @@ NOTE: Add new changes BELOW THIS COMMENT.
|
||||
[#5468]: https://github.com/AdguardTeam/AdGuardHome/issues/5468
|
||||
[#5515]: https://github.com/AdguardTeam/AdGuardHome/issues/5515
|
||||
|
||||
[rfc3696]: https://datatracker.ietf.org/doc/html/rfc3696
|
||||
[go-1.19.7]: https://groups.google.com/g/golang-announce/c/3-TpUx48iQY
|
||||
[ms-v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/milestone/62?closed=1
|
||||
[rfc3696]: https://datatracker.ietf.org/doc/html/rfc3696
|
||||
|
||||
<!--
|
||||
NOTE: Add new changes ABOVE THIS COMMENT.
|
||||
@@ -129,6 +179,7 @@ In this release, the schema version has changed from 14 to 16.
|
||||
'file_enabled': true
|
||||
'interval': '2160h'
|
||||
'size_memory': 1000
|
||||
'ignored': []
|
||||
```
|
||||
|
||||
To rollback this change, rename and move properties back into the `dns`
|
||||
@@ -1709,11 +1760,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||
|
||||
|
||||
<!--
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...HEAD
|
||||
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.27...HEAD
|
||||
[v0.107.27]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...v0.107.27
|
||||
-->
|
||||
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...HEAD
|
||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.26...HEAD
|
||||
[v0.107.26]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.25...v0.107.26
|
||||
[v0.107.25]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.24...v0.107.25
|
||||
[v0.107.24]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.23...v0.107.24
|
||||
[v0.107.23]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.22...v0.107.23
|
||||
|
||||
3
Makefile
3
Makefile
@@ -37,8 +37,6 @@ SIGN = 1
|
||||
VERSION = v0.0.0
|
||||
YARN = yarn
|
||||
|
||||
NEXTAPI = 0
|
||||
|
||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||
# into BUILD_RELEASE_DEPS_0, and so both frontend and backend
|
||||
@@ -66,7 +64,6 @@ ENV = env\
|
||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||
RACE='$(RACE)'\
|
||||
SIGN='$(SIGN)'\
|
||||
NEXTAPI='$(NEXTAPI)'\
|
||||
VERBOSE="$(VERBOSE.MACRO)"\
|
||||
VERSION='$(VERSION)'\
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# Make sure to sync any changes with the branch overrides below.
|
||||
'variables':
|
||||
'channel': 'edge'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.1'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.2'
|
||||
|
||||
'stages':
|
||||
- 'Build frontend':
|
||||
@@ -331,7 +331,7 @@
|
||||
# need to build a few of these.
|
||||
'variables':
|
||||
'channel': 'beta'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.1'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.2'
|
||||
# release-vX.Y.Z branches are the branches from which the actual final release
|
||||
# is built.
|
||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||
@@ -346,4 +346,4 @@
|
||||
# are the ones that actually get released.
|
||||
'variables':
|
||||
'channel': 'release'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.1'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.2'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
'key': 'AHBRTSPECS'
|
||||
'name': 'AdGuard Home - Build and run tests'
|
||||
'variables':
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.1'
|
||||
'dockerGo': 'adguard/golang-ubuntu:6.2'
|
||||
|
||||
'stages':
|
||||
- 'Tests':
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
"blocking_mode_refused": "REFUSED: Vastaa REFUSED-koodilla",
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Vastaa NXDOMAIN-koodilla",
|
||||
"blocking_mode_null_ip": "Tyhjä IP: Vastaa IP-nollaosoitteella (0.0.0.0 korvaa A; :: korvaa AAAA)",
|
||||
"blocking_mode_custom_ip": "Mukautettu IP: Vastaa itse määritetyllä IP-osoitteella",
|
||||
"blocking_mode_custom_ip": "Mukautettu IP: Vastaa manuaalisesti määritetyllä IP-osoitteella",
|
||||
"theme_auto": "Automaattinen",
|
||||
"theme_light": "Vaalea",
|
||||
"theme_dark": "Tumma",
|
||||
|
||||
@@ -281,6 +281,7 @@
|
||||
"blocking_mode_nxdomain": "NXDOMAIN: Svar med NXDOMAIN-koden",
|
||||
"blocking_mode_null_ip": "Null IP: Svar med en 0-IP-adresse (0.0.0.0 for A; :: for AAAA)",
|
||||
"blocking_mode_custom_ip": "Tilpasset IP: Svar med en manuelt valgt IP-adresse",
|
||||
"theme_auto": "Auto",
|
||||
"upstream_dns_client_desc": "Hvis dette feltet holdes tomt, vil AdGuard Home bruke tjenerne som er satt opp i <0>DNS-innstillingene</0>.",
|
||||
"tracker_source": "Sporerkilde",
|
||||
"source_label": "Kilde",
|
||||
|
||||
@@ -60,7 +60,7 @@ const Dashboard = ({
|
||||
title={t('refresh_btn')}
|
||||
onClick={() => getAllStats()}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#refresh" />
|
||||
</svg>
|
||||
</button>;
|
||||
|
||||
@@ -100,7 +100,7 @@ class Table extends Component {
|
||||
})
|
||||
}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -110,7 +110,7 @@ class Table extends Component {
|
||||
onClick={() => handleDelete(url)}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -162,7 +162,7 @@ const ClientCell = ({
|
||||
{content && (
|
||||
<button className={buttonArrowClass} disabled={processingRules}>
|
||||
<IconTooltip
|
||||
className="h-100"
|
||||
className="icon24"
|
||||
tooltipClass="button-action--arrow-option-container"
|
||||
xlinkHref="chevron-down"
|
||||
triggerClass="button-action--icon"
|
||||
|
||||
@@ -129,7 +129,6 @@ const Form = (props) => {
|
||||
|
||||
const onInputClear = async () => {
|
||||
setIsLoading(true);
|
||||
setDebouncedSearch(DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
change(FORM_NAMES.search, DEFAULT_LOGS_FILTER[FORM_NAMES.search]);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@@ -106,6 +106,16 @@
|
||||
max-height: 100% !important;
|
||||
}
|
||||
|
||||
.icon24 {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.icon12 {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.cursor--pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -311,7 +321,6 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button-action:active {
|
||||
|
||||
@@ -290,7 +290,7 @@ const ClientsTable = ({
|
||||
disabled={processingUpdating}
|
||||
title={t('edit_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#edit" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -301,7 +301,7 @@ const ClientsTable = ({
|
||||
disabled={processingDeleting}
|
||||
title={t('delete_table_action')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#delete" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@@ -54,6 +54,12 @@
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.service__icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
fill: #495057;
|
||||
}
|
||||
|
||||
.service--global .service__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -86,10 +86,10 @@ const Icons = () => (
|
||||
d="m19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1 -2.83 0l-.06-.06a1.65 1.65 0 0 0 -1.82-.33 1.65 1.65 0 0 0 -1 1.51v.17a2 2 0 0 1 -2 2 2 2 0 0 1 -2-2v-.09a1.65 1.65 0 0 0 -1.08-1.51 1.65 1.65 0 0 0 -1.82.33l-.06.06a2 2 0 0 1 -2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0 -1.51-1h-.17a2 2 0 0 1 -2-2 2 2 0 0 1 2-2h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0 -.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33h.08a1.65 1.65 0 0 0 1-1.51v-.17a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0 -.33 1.82v.08a1.65 1.65 0 0 0 1.51 1h.17a2 2 0 0 1 2 2 2 2 0 0 1 -2 2h-.09a1.65 1.65 0 0 0 -1.51 1z" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="refresh" viewBox="0 0 24 24" stroke="currentColor" fill="none"
|
||||
strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<path d="M23 4v6h-6M1 20v-6h6" />
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||
<symbol id="refresh" viewBox="0 0 24 24" stroke="currentColor" fill="none" strokeLinecap="round" strokeLinejoin="round" strokeWidth="2">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<polyline points="1 20 1 14 7 14"></polyline>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
|
||||
</symbol>
|
||||
|
||||
<symbol id="dns_privacy" viewBox="0 0 30 30" stroke="none" fill="currentColor"
|
||||
@@ -198,7 +198,7 @@ const Icons = () => (
|
||||
</svg>
|
||||
</symbol>
|
||||
|
||||
<symbol id="chevron-down" viewBox="0 0 24 24">
|
||||
<symbol id="chevron-down" width="24" height="24" viewBox="0 0 24 24">
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path d="M0 0h24v24H0z" fill="#878787" fillOpacity=".01" />
|
||||
<path stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"
|
||||
|
||||
@@ -39,7 +39,7 @@ const Version = () => {
|
||||
disabled={processingVersion}
|
||||
title={t('check_updates_now')}
|
||||
>
|
||||
<svg className="icons">
|
||||
<svg className="icons icon12">
|
||||
<use xlinkHref="#refresh" />
|
||||
</svg>
|
||||
</button>}
|
||||
|
||||
@@ -235,7 +235,7 @@ export default {
|
||||
"urlhaus_filter_online": {
|
||||
"name": "Malicious URL Blocklist (URLHaus)",
|
||||
"categoryId": "security",
|
||||
"homepage": "https://gitlab.com/malware-filter/urlhaus-filter",
|
||||
"homepage": "https://urlhaus.abuse.ch/",
|
||||
"source": "https://adguardteam.github.io/HostlistsRegistry/assets/filter_11.txt"
|
||||
},
|
||||
"windowsspyblocker_hosts_spy_rules": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"timeUpdated": "2023-03-01T10:05:51.445Z",
|
||||
"timeUpdated": "2023-03-08T00:09:48.692Z",
|
||||
"categories": {
|
||||
"0": "audio_video_player",
|
||||
"1": "comments",
|
||||
@@ -19316,6 +19316,13 @@
|
||||
"companyId": null,
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"edgio": {
|
||||
"name": "Edgio",
|
||||
"categoryId": 9,
|
||||
"url": "https://edg.io/",
|
||||
"companyId": "edgio",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"element": {
|
||||
"name": "Element",
|
||||
"categoryId": 7,
|
||||
@@ -19372,6 +19379,13 @@
|
||||
"companyId": "lets_encrypt",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"lgtv": {
|
||||
"name": "LG TV",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.lg.com/",
|
||||
"companyId": "lgcorp",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"matrix": {
|
||||
"name": "Matrix",
|
||||
"categoryId": 5,
|
||||
@@ -19407,6 +19421,13 @@
|
||||
"companyId": "mozilla",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"nab": {
|
||||
"name": "National Australia Bank",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.nab.com.au/",
|
||||
"companyId": "nab",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"notion": {
|
||||
"name": "Notion",
|
||||
"categoryId": 8,
|
||||
@@ -19456,6 +19477,13 @@
|
||||
"companyId": "qualcomm",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"recaptcha": {
|
||||
"name": "reCAPTCHA",
|
||||
"categoryId": 8,
|
||||
"url": "https://www.google.com/recaptcha/about/",
|
||||
"companyId": "google",
|
||||
"source": "AdGuard"
|
||||
},
|
||||
"sectigo": {
|
||||
"name": "Sectigo Limited",
|
||||
"categoryId": 5,
|
||||
@@ -23893,6 +23921,11 @@
|
||||
"adguardvpn.com": "adguard_vpn",
|
||||
"adguard-vpn.com": "adguard_vpn",
|
||||
"adguard-vpn.online": "adguard_vpn",
|
||||
"adjust.net.in": "adjust",
|
||||
"adj.st": "adjust",
|
||||
"adjust.io": "adjust",
|
||||
"adjust.world": "adjust",
|
||||
"apptrace.com": "adjust",
|
||||
"akadns.net": "akamai_technologies",
|
||||
"akamaiedge.net": "akamai_technologies",
|
||||
"akaquill.net": "akamai_technologies",
|
||||
@@ -23907,6 +23940,21 @@
|
||||
"aliyun.com": "alibaba_cloud",
|
||||
"ucweb.com": "alibaba_ucbrowser",
|
||||
"alipayobjects.com": "alipay.com",
|
||||
"amazoncrl.com": "amazon",
|
||||
"aamazoncognito.com": "amazon",
|
||||
"amazonbrowserapp.es": "amazon",
|
||||
"amazonbrowserapp.co.uk": "amazon",
|
||||
"amazon.sa": "amazon",
|
||||
"amazon.nl": "amazon",
|
||||
"amazon.in": "amazon",
|
||||
"amazon.com.mx": "amazon",
|
||||
"amazon.com.au": "amazon",
|
||||
"amazon-corp.com": "amazon",
|
||||
"a2z.com": "amazon",
|
||||
"amazontrust.com": "amazon_cdn",
|
||||
"associates-amazon.com": "amazon_cdn",
|
||||
"amazonpay.in": "amazon_payments",
|
||||
"amazonvideo.com": "amazon_video",
|
||||
"taobao.com": "taobao",
|
||||
"appcenter.ms": "appcenter",
|
||||
"iadsdk.apple.com": "apple_ads",
|
||||
@@ -23926,6 +23974,8 @@
|
||||
"apple-livephotoskit.com": "apple",
|
||||
"safebrowsing.apple": "apple",
|
||||
"safebrowsing.g.applimg.com": "apple",
|
||||
"applvn.com": "applovin",
|
||||
"applovin.com": "applovin",
|
||||
"blob.core.windows.net": "azure_blob_storage",
|
||||
"azure.com": "azure",
|
||||
"trafficmanager.net": "azure",
|
||||
@@ -23935,12 +23985,21 @@
|
||||
"cloudflare-dns.com": "cloudflare",
|
||||
"crashlytics.com": "crashlytics",
|
||||
"phicdn.net": "digicert_trust_seal",
|
||||
"alphacdn.net": "edgio",
|
||||
"edg.io": "edgio",
|
||||
"edgecast.com": "edgio",
|
||||
"edgecastcdn.net": "edgio",
|
||||
"edgecastdns.net": "edgio",
|
||||
"sigmacdn.net": "edgio",
|
||||
"element.io": "element",
|
||||
"riot.im": "element",
|
||||
"app-measurement.com": "firebase",
|
||||
"flipboard.com": "flipboard",
|
||||
"flurry.com": "flurry",
|
||||
"ghcr.io": "github",
|
||||
"github.dev": "github",
|
||||
"gmail.com": "gmail",
|
||||
"googlehosted.com": "google_appspot",
|
||||
"gvt1.com": "google_servers",
|
||||
"gvt2.com": "google_servers",
|
||||
"gvt3.com": "google_servers",
|
||||
@@ -23949,10 +24008,15 @@
|
||||
"kik.com": "kik",
|
||||
"apikik.com": "kik",
|
||||
"kik-live.com": "kik",
|
||||
"letsencrypt.org": "lets_encrypt",
|
||||
"slatic.net": "lazada",
|
||||
"lencr.org": "lets_encrypt",
|
||||
"edgecastcdn.net": "markmonitor",
|
||||
"letsencrypt.org": "lets_encrypt",
|
||||
"lgsmartad.com": "lgtv",
|
||||
"lgtvcommon.com": "lgtv",
|
||||
"lgtvsdp.com": "lgtv",
|
||||
"lge.com": "lgtv",
|
||||
"lg.com": "lgtv",
|
||||
"markmonitor.com": "markmonitor",
|
||||
"matrix.org": "matrix",
|
||||
"medialab.la": "medialab",
|
||||
"media-lab.ai": "medialab",
|
||||
@@ -23968,6 +24032,13 @@
|
||||
"mozilla.net": "mozilla",
|
||||
"mozilla.org": "mozilla",
|
||||
"nflximg.com": "netflix",
|
||||
"nab.com": "nab",
|
||||
"nab.com.au": "nab",
|
||||
"nab.net": "nab",
|
||||
"nabgroup.com": "nab",
|
||||
"national.com.au": "nab",
|
||||
"nationalaustraliabank.com.au": "nab",
|
||||
"nationalbank.com.au": "nab",
|
||||
"notion.so": "notion",
|
||||
"ntp.org": "ntppool",
|
||||
"ntppool.org": "ntppool",
|
||||
@@ -23984,6 +24055,7 @@
|
||||
"plex.direct": "plex",
|
||||
"xtracloud.net": "qualcomm",
|
||||
"qualcomm.com": "qualcomm",
|
||||
"recaptcha.net": "recaptcha",
|
||||
"sectigo.com": "sectigo",
|
||||
"showrss.info": "showrss",
|
||||
"similarweb.io": "similarweb",
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package aghtest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"net"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -118,36 +116,6 @@ func (w *FSWatcher) Close() (err error) {
|
||||
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
|
||||
|
||||
// Package upstream
|
||||
|
||||
@@ -263,15 +263,12 @@ func (s *v4Server) prepareOptions() {
|
||||
|
||||
// IP-Layer Per Interface
|
||||
|
||||
// Since nearly all networks in the Internet currently support an MTU of
|
||||
// 576 or greater, we strongly recommend the use of 576 for datagrams
|
||||
// sent to non-local networks.
|
||||
// Don't set the Interface MTU because client may choose the value on
|
||||
// their own since it's listed in the [Host Requirements RFC]. It also
|
||||
// seems the values listed there sometimes appear obsolete, see
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/5281.
|
||||
//
|
||||
// See https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
|
||||
dhcpv4.Option{
|
||||
Code: dhcpv4.OptionInterfaceMTU,
|
||||
Value: dhcpv4.Uint16(576),
|
||||
},
|
||||
// [Host Requirements RFC]: https://datatracker.ietf.org/doc/html/rfc1122#section-3.3.3.
|
||||
|
||||
// Set the All Subnets Are Local Option to false since commonly the
|
||||
// connected hosts aren't expected to be multihomed.
|
||||
|
||||
@@ -53,7 +53,6 @@ const (
|
||||
// The zero FilteringConfig is empty and ready for use.
|
||||
type FilteringConfig struct {
|
||||
// Callbacks for other modules
|
||||
// --
|
||||
|
||||
// FilterHandler is an optional additional filtering callback.
|
||||
FilterHandler func(clientAddr net.IP, clientID string, settings *filtering.Settings) `yaml:"-"`
|
||||
@@ -64,50 +63,82 @@ type FilteringConfig struct {
|
||||
GetCustomUpstreamByClient func(id string) (conf *proxy.UpstreamConfig, err error) `yaml:"-"`
|
||||
|
||||
// Protection configuration
|
||||
// --
|
||||
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of filtering features
|
||||
BlockingMode BlockingMode `yaml:"blocking_mode"` // mode how to answer filtered requests
|
||||
BlockingIPv4 net.IP `yaml:"blocking_ipv4"` // IP address to be returned for a blocked A request
|
||||
BlockingIPv6 net.IP `yaml:"blocking_ipv6"` // IP address to be returned for a blocked AAAA request
|
||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
|
||||
// ProtectionEnabled defines whether or not use any of filtering features.
|
||||
ProtectionEnabled bool `yaml:"protection_enabled"`
|
||||
|
||||
// IP (or domain name) which is used to respond to DNS requests blocked by parental control or safe-browsing
|
||||
ParentalBlockHost string `yaml:"parental_block_host"`
|
||||
// BlockingMode defines the way how blocked responses are constructed.
|
||||
BlockingMode BlockingMode `yaml:"blocking_mode"`
|
||||
|
||||
// BlockingIPv4 is the IP address to be returned for a blocked A request.
|
||||
BlockingIPv4 net.IP `yaml:"blocking_ipv4"`
|
||||
|
||||
// BlockingIPv6 is the IP address to be returned for a blocked AAAA
|
||||
// request.
|
||||
BlockingIPv6 net.IP `yaml:"blocking_ipv6"`
|
||||
|
||||
// BlockedResponseTTL is the time-to-live value for blocked responses. If
|
||||
// 0, then default value is used (3600).
|
||||
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"`
|
||||
|
||||
// ParentalBlockHost is the IP (or domain name) which is used to respond to
|
||||
// DNS requests blocked by parental control.
|
||||
ParentalBlockHost string `yaml:"parental_block_host"`
|
||||
|
||||
// SafeBrowsingBlockHost is the IP (or domain name) which is used to
|
||||
// respond to DNS requests blocked by safe-browsing.
|
||||
SafeBrowsingBlockHost string `yaml:"safebrowsing_block_host"`
|
||||
|
||||
// Anti-DNS amplification
|
||||
// --
|
||||
|
||||
Ratelimit uint32 `yaml:"ratelimit"` // max number of requests per second from a given IP (0 to disable)
|
||||
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"` // a list of whitelisted client IP addresses
|
||||
RefuseAny bool `yaml:"refuse_any"` // if true, refuse ANY requests
|
||||
// Ratelimit is the maximum number of requests per second from a given IP
|
||||
// (0 to disable).
|
||||
Ratelimit uint32 `yaml:"ratelimit"`
|
||||
|
||||
// RatelimitWhitelist is the list of whitelisted client IP addresses.
|
||||
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"`
|
||||
|
||||
// RefuseAny, if true, refuse ANY requests.
|
||||
RefuseAny bool `yaml:"refuse_any"`
|
||||
|
||||
// Upstream DNS servers configuration
|
||||
// --
|
||||
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
UpstreamDNSFileName string `yaml:"upstream_dns_file"`
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"` // a list of bootstrap DNS for DoH and DoT (plain DNS only)
|
||||
AllServers bool `yaml:"all_servers"` // if true, parallel queries to all configured upstream servers are enabled
|
||||
FastestAddr bool `yaml:"fastest_addr"` // use Fastest Address algorithm
|
||||
// UpstreamDNS is the list of upstream DNS servers.
|
||||
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||
|
||||
// UpstreamDNSFileName, if set, points to the file which contains upstream
|
||||
// DNS servers.
|
||||
UpstreamDNSFileName string `yaml:"upstream_dns_file"`
|
||||
|
||||
// BootstrapDNS is the list of bootstrap DNS servers for DoH and DoT
|
||||
// resolvers (plain DNS only).
|
||||
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||
|
||||
// AllServers, if true, parallel queries to all configured upstream servers
|
||||
// are enabled.
|
||||
AllServers bool `yaml:"all_servers"`
|
||||
|
||||
// FastestAddr, if true, use Fastest Address algorithm.
|
||||
FastestAddr bool `yaml:"fastest_addr"`
|
||||
|
||||
// FastestTimeout replaces the default timeout for dialing IP addresses
|
||||
// when FastestAddr is true.
|
||||
FastestTimeout timeutil.Duration `yaml:"fastest_timeout"`
|
||||
|
||||
// Access settings
|
||||
// --
|
||||
|
||||
// AllowedClients is the slice of IP addresses, CIDR networks, and ClientIDs
|
||||
// of allowed clients. If not empty, only these clients are allowed, and
|
||||
// [FilteringConfig.DisallowedClients] are ignored.
|
||||
// AllowedClients is the slice of IP addresses, CIDR networks, and
|
||||
// ClientIDs of allowed clients. If not empty, only these clients are
|
||||
// allowed, and [FilteringConfig.DisallowedClients] are ignored.
|
||||
AllowedClients []string `yaml:"allowed_clients"`
|
||||
|
||||
// DisallowedClients is the slice of IP addresses, CIDR networks, and
|
||||
// ClientIDs of disallowed clients.
|
||||
DisallowedClients []string `yaml:"disallowed_clients"`
|
||||
|
||||
BlockedHosts []string `yaml:"blocked_hosts"` // hosts that should be blocked
|
||||
// BlockedHosts is the list of hosts that should be blocked.
|
||||
BlockedHosts []string `yaml:"blocked_hosts"`
|
||||
|
||||
// TrustedProxies is the list of IP addresses and CIDR networks to detect
|
||||
// proxy servers addresses the DoH requests from which should be handled.
|
||||
// The value of nil or an empty slice for this field makes Proxy not trust
|
||||
@@ -115,26 +146,46 @@ type FilteringConfig struct {
|
||||
TrustedProxies []string `yaml:"trusted_proxies"`
|
||||
|
||||
// DNS cache settings
|
||||
// --
|
||||
|
||||
CacheSize uint32 `yaml:"cache_size"` // DNS cache size (in bytes)
|
||||
CacheMinTTL uint32 `yaml:"cache_ttl_min"` // override TTL value (minimum) received from upstream server
|
||||
CacheMaxTTL uint32 `yaml:"cache_ttl_max"` // override TTL value (maximum) received from upstream server
|
||||
// CacheSize is the DNS cache size (in bytes).
|
||||
CacheSize uint32 `yaml:"cache_size"`
|
||||
|
||||
// CacheMinTTL is the override TTL value (minimum) received from upstream
|
||||
// server.
|
||||
CacheMinTTL uint32 `yaml:"cache_ttl_min"`
|
||||
|
||||
// CacheMaxTTL is the override TTL value (maximum) received from upstream
|
||||
// server.
|
||||
CacheMaxTTL uint32 `yaml:"cache_ttl_max"`
|
||||
|
||||
// CacheOptimistic defines if optimistic cache mechanism should be used.
|
||||
CacheOptimistic bool `yaml:"cache_optimistic"`
|
||||
|
||||
// Other settings
|
||||
// --
|
||||
|
||||
BogusNXDomain []string `yaml:"bogus_nxdomain"` // transform responses with these IP addresses to NXDOMAIN
|
||||
AAAADisabled bool `yaml:"aaaa_disabled"` // Respond with an empty answer to all AAAA requests
|
||||
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set AD flag in outcoming DNS request
|
||||
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
|
||||
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests
|
||||
HandleDDR bool `yaml:"handle_ddr"` // Handle DDR requests
|
||||
// BogusNXDomain is the list of IP addresses, responses with them will be
|
||||
// transformed to NXDOMAIN.
|
||||
BogusNXDomain []string `yaml:"bogus_nxdomain"`
|
||||
|
||||
// IpsetList is the ipset configuration that allows AdGuard Home to add
|
||||
// IP addresses of the specified domain names to an ipset list. Syntax:
|
||||
// AAAADisabled, if true, respond with an empty answer to all AAAA
|
||||
// requests.
|
||||
AAAADisabled bool `yaml:"aaaa_disabled"`
|
||||
|
||||
// EnableDNSSEC, if true, set AD flag in outcoming DNS request.
|
||||
EnableDNSSEC bool `yaml:"enable_dnssec"`
|
||||
|
||||
// EDNSClientSubnet is the settings list for EDNS Client Subnet.
|
||||
EDNSClientSubnet *EDNSClientSubnet `yaml:"edns_client_subnet"`
|
||||
|
||||
// MaxGoroutines is the max number of parallel goroutines for processing
|
||||
// incoming requests.
|
||||
MaxGoroutines uint32 `yaml:"max_goroutines"`
|
||||
|
||||
// HandleDDR, if true, handle DDR requests
|
||||
HandleDDR bool `yaml:"handle_ddr"`
|
||||
|
||||
// IpsetList is the ipset configuration that allows AdGuard Home to add IP
|
||||
// addresses of the specified domain names to an ipset list. Syntax:
|
||||
//
|
||||
// DOMAIN[,DOMAIN].../IPSET_NAME
|
||||
//
|
||||
@@ -146,6 +197,18 @@ type FilteringConfig struct {
|
||||
IpsetListFileName string `yaml:"ipset_file"`
|
||||
}
|
||||
|
||||
// EDNSClientSubnet is the settings list for EDNS Client Subnet.
|
||||
type EDNSClientSubnet struct {
|
||||
// CustomIP for EDNS Client Subnet.
|
||||
CustomIP string `yaml:"custom_ip"`
|
||||
|
||||
// Enabled defines if EDNS Client Subnet is enabled.
|
||||
Enabled bool `yaml:"enabled"`
|
||||
|
||||
// UseCustom defines if CustomIP should be used.
|
||||
UseCustom bool `yaml:"use_custom"`
|
||||
}
|
||||
|
||||
// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
|
||||
type TLSConfig struct {
|
||||
cert tls.Certificate
|
||||
@@ -270,12 +333,24 @@ func (s *Server) createProxyConfig() (conf proxy.Config, err error) {
|
||||
UpstreamConfig: srvConf.UpstreamConfig,
|
||||
BeforeRequestHandler: s.beforeRequestHandler,
|
||||
RequestHandler: s.handleDNSRequest,
|
||||
EnableEDNSClientSubnet: srvConf.EnableEDNSClientSubnet,
|
||||
EnableEDNSClientSubnet: srvConf.EDNSClientSubnet.Enabled,
|
||||
MaxGoroutines: int(srvConf.MaxGoroutines),
|
||||
UseDNS64: srvConf.UseDNS64,
|
||||
DNS64Prefs: srvConf.DNS64Prefixes,
|
||||
}
|
||||
|
||||
if srvConf.EDNSClientSubnet.UseCustom {
|
||||
// TODO(s.chzhen): Add wrapper around netip.Addr.
|
||||
var ip net.IP
|
||||
ip, err = netutil.ParseIP(srvConf.EDNSClientSubnet.CustomIP)
|
||||
if err != nil {
|
||||
return conf, fmt.Errorf("edns: %w", err)
|
||||
}
|
||||
|
||||
// TODO(s.chzhen): Use netip.Addr instead of net.IP inside dnsproxy.
|
||||
conf.EDNSAddr = ip
|
||||
}
|
||||
|
||||
if srvConf.CacheSize != 0 {
|
||||
conf.CacheEnabled = true
|
||||
conf.CacheSizeBytes = int(srvConf.CacheSize)
|
||||
|
||||
@@ -287,6 +287,9 @@ func TestServer_HandleDNSRequest_dns64(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UseDNS64: true,
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, localUps)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
@@ -467,6 +467,11 @@ func TestServer_ProcessRestrictLocal(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
// TODO(s.chzhen): Add tests where EDNSClientSubnet.Enabled is true.
|
||||
// Improve FilteringConfig declaration for tests.
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, ups)
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{ups}
|
||||
startDeferStop(t, s)
|
||||
@@ -539,6 +544,9 @@ func TestServer_ProcessLocalPTR_usingResolvers(t *testing.T) {
|
||||
ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
},
|
||||
aghtest.NewUpstreamMock(func(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||
return aghalg.Coalesce(
|
||||
|
||||
@@ -156,6 +156,9 @@ func createTestTLS(t *testing.T, tlsConf TLSConfig) (s *Server, certPem []byte)
|
||||
s = createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, nil)
|
||||
|
||||
tlsConf.CertificateChainData, tlsConf.PrivateKeyData = certPem, keyPem
|
||||
@@ -267,6 +270,9 @@ func TestServer(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, nil)
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
|
||||
startDeferStop(t, s)
|
||||
@@ -305,7 +311,8 @@ func TestServer_timeout(t *testing.T) {
|
||||
srvConf := &ServerConfig{
|
||||
UpstreamTimeout: timeout,
|
||||
FilteringConfig: FilteringConfig{
|
||||
BlockingMode: BlockingModeDefault,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -323,6 +330,9 @@ func TestServer_timeout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
s.conf.FilteringConfig.BlockingMode = BlockingModeDefault
|
||||
s.conf.FilteringConfig.EDNSClientSubnet = &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
}
|
||||
err = s.Prepare(&s.conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -334,6 +344,9 @@ func TestServerWithProtectionDisabled(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, nil)
|
||||
s.conf.UpstreamConfig.Upstreams = []upstream.Upstream{newGoogleUpstream()}
|
||||
startDeferStop(t, s)
|
||||
@@ -438,6 +451,9 @@ func TestSafeSearch(t *testing.T) {
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, filterConf, forwardConf, nil)
|
||||
@@ -493,6 +509,11 @@ func TestInvalidRequest(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
startDeferStop(t, s)
|
||||
|
||||
@@ -519,6 +540,9 @@ func TestBlockedRequest(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
|
||||
@@ -544,6 +568,9 @@ func TestServerCustomClientUpstream(t *testing.T) {
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
|
||||
@@ -592,6 +619,11 @@ func TestBlockCNAMEProtectionEnabled(t *testing.T) {
|
||||
s := createTestServer(t, &filtering.Config{}, ServerConfig{
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
testUpstm := &aghtest.Upstream{
|
||||
CName: testCNAMEs,
|
||||
@@ -622,6 +654,9 @@ func TestBlockCNAME(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
|
||||
@@ -691,6 +726,9 @@ func TestClientRulesForCNAMEMatching(t *testing.T) {
|
||||
FilterHandler: func(_ net.IP, _ string, settings *filtering.Settings) {
|
||||
settings.FilteringEnabled = false
|
||||
},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
|
||||
@@ -732,6 +770,9 @@ func TestNullBlockedRequest(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeNullIP,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, &filtering.Config{}, forwardConf, nil)
|
||||
@@ -784,6 +825,9 @@ func TestBlockedCustomIP(t *testing.T) {
|
||||
BlockingMode: BlockingModeCustomIP,
|
||||
BlockingIPv4: nil,
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -832,6 +876,9 @@ func TestBlockedByHosts(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -865,6 +912,9 @@ func TestBlockedBySafeBrowsing(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
SafeBrowsingBlockHost: ans4.String(),
|
||||
ProtectionEnabled: true,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
s := createTestServer(t, filterConf, forwardConf, nil)
|
||||
@@ -919,6 +969,9 @@ func TestRewrite(t *testing.T) {
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
UpstreamDNS: []string{"8.8.8.8:53"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1033,6 +1086,7 @@ func TestPTRResponseFromDHCPLeases(t *testing.T) {
|
||||
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
|
||||
s.conf.FilteringConfig.ProtectionEnabled = true
|
||||
s.conf.FilteringConfig.BlockingMode = BlockingModeDefault
|
||||
s.conf.FilteringConfig.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
|
||||
|
||||
err = s.Prepare(&s.conf)
|
||||
require.NoError(t, err)
|
||||
@@ -1108,6 +1162,7 @@ func TestPTRResponseFromHosts(t *testing.T) {
|
||||
s.conf.TCPListenAddrs = []*net.TCPAddr{{}}
|
||||
s.conf.UpstreamDNS = []string{"127.0.0.1:53"}
|
||||
s.conf.FilteringConfig.BlockingMode = BlockingModeDefault
|
||||
s.conf.FilteringConfig.EDNSClientSubnet = &EDNSClientSubnet{Enabled: false}
|
||||
|
||||
err = s.Prepare(&s.conf)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -29,6 +29,9 @@ func TestHandleDNSRequest_filterDNSResponse(t *testing.T) {
|
||||
FilteringConfig: FilteringConfig{
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
EDNSClientSubnet: &EDNSClientSubnet{
|
||||
Enabled: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
filters := []filtering.Filter{{
|
||||
|
||||
@@ -57,7 +57,7 @@ func (s *Server) getDNSConfig() (c *jsonDNSConfig) {
|
||||
blockingIPv4 := s.conf.BlockingIPv4
|
||||
blockingIPv6 := s.conf.BlockingIPv6
|
||||
ratelimit := s.conf.Ratelimit
|
||||
enableEDNSClientSubnet := s.conf.EnableEDNSClientSubnet
|
||||
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
|
||||
enableDNSSEC := s.conf.EnableDNSSEC
|
||||
aaaaDisabled := s.conf.AAAADisabled
|
||||
cacheSize := s.conf.CacheSize
|
||||
@@ -280,7 +280,7 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
|
||||
setIfNotNil(&s.conf.LocalPTRResolvers, dc.LocalPTRUpstreams),
|
||||
setIfNotNil(&s.conf.UpstreamDNSFileName, dc.UpstreamsFile),
|
||||
setIfNotNil(&s.conf.BootstrapDNS, dc.Bootstraps),
|
||||
setIfNotNil(&s.conf.EnableEDNSClientSubnet, dc.EDNSCSEnabled),
|
||||
setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),
|
||||
setIfNotNil(&s.conf.CacheSize, dc.CacheSize),
|
||||
setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
|
||||
setIfNotNil(&s.conf.CacheMaxTTL, dc.CacheMaxTTL),
|
||||
|
||||
@@ -69,6 +69,7 @@ func TestDNSForwardHTTP_handleGetConfig(t *testing.T) {
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ConfigModified: func() {},
|
||||
}
|
||||
@@ -144,6 +145,7 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
ProtectionEnabled: true,
|
||||
BlockingMode: BlockingModeDefault,
|
||||
UpstreamDNS: []string{"8.8.8.8:53", "8.8.4.4:53"},
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
ConfigModified: func() {},
|
||||
}
|
||||
@@ -227,7 +229,10 @@ func TestDNSForwardHTTP_handleSetConfig(t *testing.T) {
|
||||
require.True(t, ok)
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Cleanup(func() { s.conf = defaultConf })
|
||||
t.Cleanup(func() {
|
||||
s.conf = defaultConf
|
||||
s.conf.FilteringConfig.EDNSClientSubnet.Enabled = false
|
||||
})
|
||||
|
||||
rBody := io.NopCloser(bytes.NewReader(caseData.Req))
|
||||
var r *http.Request
|
||||
@@ -443,6 +448,9 @@ func TestServer_handleTestUpstreaDNS(t *testing.T) {
|
||||
UDPListenAddrs: []*net.UDPAddr{{}},
|
||||
TCPListenAddrs: []*net.TCPAddr{{}},
|
||||
UpstreamTimeout: upsTimeout,
|
||||
FilteringConfig: FilteringConfig{
|
||||
EDNSClientSubnet: &EDNSClientSubnet{Enabled: false},
|
||||
},
|
||||
}, nil)
|
||||
startDeferStop(t, srv)
|
||||
|
||||
|
||||
@@ -13,8 +13,37 @@ import (
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
)
|
||||
|
||||
// SafeSearch interface describes a service for search engines hosts rewrites.
|
||||
type SafeSearch interface {
|
||||
// SearchHost returns a replacement address for the search engine host.
|
||||
SearchHost(host string, qtype uint16) (res *rules.DNSRewrite)
|
||||
|
||||
// CheckHost checks host with safe search engine.
|
||||
CheckHost(host string, qtype uint16) (res Result, err error)
|
||||
}
|
||||
|
||||
// SafeSearchConfig is a struct with safe search related settings.
|
||||
type SafeSearchConfig struct {
|
||||
// CustomResolver is the resolver used by safe search.
|
||||
CustomResolver Resolver `yaml:"-"`
|
||||
|
||||
// Enabled indicates if safe search is enabled entirely.
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
|
||||
// Services flags. Each flag indicates if the corresponding service is
|
||||
// enabled or disabled.
|
||||
|
||||
Bing bool `yaml:"bing" json:"bing"`
|
||||
DuckDuckGo bool `yaml:"duckduckgo" json:"duckduckgo"`
|
||||
Google bool `yaml:"google" json:"google"`
|
||||
Pixabay bool `yaml:"pixabay" json:"pixabay"`
|
||||
Yandex bool `yaml:"yandex" json:"yandex"`
|
||||
YouTube bool `yaml:"youtube" json:"youtube"`
|
||||
}
|
||||
|
||||
/*
|
||||
expire byte[4]
|
||||
res Result
|
||||
|
||||
34
internal/filtering/safesearch/rules.go
Normal file
34
internal/filtering/safesearch/rules.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package safesearch
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed rules/bing.txt
|
||||
var bing string
|
||||
|
||||
//go:embed rules/google.txt
|
||||
var google string
|
||||
|
||||
//go:embed rules/pixabay.txt
|
||||
var pixabay string
|
||||
|
||||
//go:embed rules/duckduckgo.txt
|
||||
var duckduckgo string
|
||||
|
||||
//go:embed rules/yandex.txt
|
||||
var yandex string
|
||||
|
||||
//go:embed rules/youtube.txt
|
||||
var youtube string
|
||||
|
||||
// safeSearchRules is a map with rules texts grouped by search providers.
|
||||
// Source rules downloaded from:
|
||||
// https://adguardteam.github.io/HostlistsRegistry/assets/engines_safe_search.txt,
|
||||
// https://adguardteam.github.io/HostlistsRegistry/assets/youtube_safe_search.txt.
|
||||
var safeSearchRules = map[Service]string{
|
||||
Bing: bing,
|
||||
DuckDuckGo: duckduckgo,
|
||||
Google: google,
|
||||
Pixabay: pixabay,
|
||||
Yandex: yandex,
|
||||
YouTube: youtube,
|
||||
}
|
||||
1
internal/filtering/safesearch/rules/bing.txt
Normal file
1
internal/filtering/safesearch/rules/bing.txt
Normal file
@@ -0,0 +1 @@
|
||||
|www.bing.com^$dnsrewrite=NOERROR;CNAME;strict.bing.com
|
||||
3
internal/filtering/safesearch/rules/duckduckgo.txt
Normal file
3
internal/filtering/safesearch/rules/duckduckgo.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
|duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|start.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
|www.duckduckgo.com^$dnsrewrite=NOERROR;CNAME;safe.duckduckgo.com
|
||||
191
internal/filtering/safesearch/rules/google.txt
Normal file
191
internal/filtering/safesearch/rules/google.txt
Normal file
@@ -0,0 +1,191 @@
|
||||
|www.google.ad^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ae^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.al^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.am^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.as^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.at^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.az^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ba^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.be^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bs^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.bt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.by^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ca^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cat^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cd^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ch^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ci^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ao^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.bw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ck^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.cr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.id^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.il^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.in^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.jp^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ke^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.kr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ls^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ma^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.mz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.nz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.th^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.tz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ug^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.uk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.uz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.ve^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.co.vi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.af^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ag^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ai^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ar^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.au^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.bd^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.bh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.bn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.bo^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.br^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.bz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.co^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.cu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.cy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.do^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ec^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.eg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.et^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.fj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.gh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.gi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.gt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.hk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.jm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.kh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.kw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.lb^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ly^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.mm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.mt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.mx^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.my^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.na^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.nf^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ng^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ni^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.np^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.om^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.pa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.pe^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.pg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ph^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.pk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.pr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.py^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.qa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.sa^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.sb^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.sg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.sl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.sv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.tj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.tr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.tw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.ua^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.uy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.vc^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com.vn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.com^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.cz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.de^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.dj^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.dk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.dm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.dz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ee^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.es^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.fi^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.fm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.fr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ga^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ge^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gp^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.gy^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.hn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.hr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ht^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.hu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ie^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.im^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.iq^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.is^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.it^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.je^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.jo^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.kg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ki^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.kz^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.la^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.li^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.lk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.lt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.lu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.lv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.md^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.me^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ml^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ms^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mv^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.mw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ne^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.nl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.no^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.nr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.nu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.pl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.pn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ps^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.pt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ro^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.rs^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ru^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.rw^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sc^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.se^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sh^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.si^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.so^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.sr^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.st^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.td^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tk^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tl^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tm^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tn^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.to^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.tt^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.vg^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.vu^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
|www.google.ws^$dnsrewrite=NOERROR;CNAME;forcesafesearch.google.com
|
||||
1
internal/filtering/safesearch/rules/pixabay.txt
Normal file
1
internal/filtering/safesearch/rules/pixabay.txt
Normal file
@@ -0,0 +1 @@
|
||||
|pixabay.com^$dnsrewrite=NOERROR;CNAME;safesearch.pixabay.com
|
||||
52
internal/filtering/safesearch/rules/yandex.txt
Normal file
52
internal/filtering/safesearch/rules/yandex.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
|www.xn--d1acpjx3f.xn--p1ai^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.ya.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.az^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.by^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.co.il^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.com.am^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.com.ge^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.com.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.com.tr^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.com^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.de^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.ee^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.eu^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.fi^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.fr^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.kz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.lt^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.lv^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.md^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.net^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.org^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.pl^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|www.yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|xn--d1acpjx3f.xn--p1ai^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|ya.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.az^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.by^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.co.il^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.com.am^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.com.ge^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.com.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.com.tr^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.com^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.de^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.ee^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.eu^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.fi^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.fr^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.kz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.lt^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.lv^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.md^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.net^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.org^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.pl^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.ru^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.tj^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.tm^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
|yandex.uz^$dnsrewrite=NOERROR;A;213.180.193.56
|
||||
5
internal/filtering/safesearch/rules/youtube.txt
Normal file
5
internal/filtering/safesearch/rules/youtube.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
|www.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|m.youtube.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtubei.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|youtube.googleapis.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
|www.youtube-nocookie.com^$dnsrewrite=NOERROR;CNAME;restrictmoderate.youtube.com
|
||||
269
internal/filtering/safesearch/safesearch.go
Normal file
269
internal/filtering/safesearch/safesearch.go
Normal file
@@ -0,0 +1,269 @@
|
||||
// Package safesearch implements safesearch host matching.
|
||||
package safesearch
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/golibs/cache"
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
"github.com/AdguardTeam/urlfilter"
|
||||
"github.com/AdguardTeam/urlfilter/filterlist"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Service is a enum with service names used as search providers.
|
||||
type Service string
|
||||
|
||||
// Service enum members.
|
||||
const (
|
||||
Bing Service = "bing"
|
||||
DuckDuckGo Service = "duckduckgo"
|
||||
Google Service = "google"
|
||||
Pixabay Service = "pixabay"
|
||||
Yandex Service = "yandex"
|
||||
YouTube Service = "youtube"
|
||||
)
|
||||
|
||||
// isServiceProtected returns true if the service safe search is active.
|
||||
func isServiceProtected(s filtering.SafeSearchConfig, service Service) (ok bool) {
|
||||
switch service {
|
||||
case Bing:
|
||||
return s.Bing
|
||||
case DuckDuckGo:
|
||||
return s.DuckDuckGo
|
||||
case Google:
|
||||
return s.Google
|
||||
case Pixabay:
|
||||
return s.Pixabay
|
||||
case Yandex:
|
||||
return s.Yandex
|
||||
case YouTube:
|
||||
return s.YouTube
|
||||
default:
|
||||
panic(fmt.Errorf("safesearch: invalid sources: not found service %q", service))
|
||||
}
|
||||
}
|
||||
|
||||
// DefaultSafeSearch is the default safesearch struct.
|
||||
type DefaultSafeSearch struct {
|
||||
engine *urlfilter.DNSEngine
|
||||
safeSearchCache cache.Cache
|
||||
resolver filtering.Resolver
|
||||
cacheTime time.Duration
|
||||
}
|
||||
|
||||
// NewDefaultSafeSearch returns new safesearch struct. CacheTime is an element
|
||||
// TTL (in minutes).
|
||||
func NewDefaultSafeSearch(
|
||||
conf filtering.SafeSearchConfig,
|
||||
cacheSize uint,
|
||||
cacheTime time.Duration,
|
||||
) (ss *DefaultSafeSearch, err error) {
|
||||
engine, err := newEngine(filtering.SafeSearchListID, conf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resolver filtering.Resolver = net.DefaultResolver
|
||||
if conf.CustomResolver != nil {
|
||||
resolver = conf.CustomResolver
|
||||
}
|
||||
|
||||
return &DefaultSafeSearch{
|
||||
engine: engine,
|
||||
safeSearchCache: cache.New(cache.Config{
|
||||
EnableLRU: true,
|
||||
MaxSize: cacheSize,
|
||||
}),
|
||||
cacheTime: cacheTime,
|
||||
resolver: resolver,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newEngine creates new engine for provided safe search configuration.
|
||||
func newEngine(listID int, conf filtering.SafeSearchConfig) (engine *urlfilter.DNSEngine, err error) {
|
||||
var sb strings.Builder
|
||||
for service, serviceRules := range safeSearchRules {
|
||||
if isServiceProtected(conf, service) {
|
||||
sb.WriteString(serviceRules)
|
||||
}
|
||||
}
|
||||
|
||||
strList := &filterlist.StringRuleList{
|
||||
ID: listID,
|
||||
RulesText: sb.String(),
|
||||
IgnoreCosmetic: true,
|
||||
}
|
||||
|
||||
rs, err := filterlist.NewRuleStorage([]filterlist.RuleList{strList})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating rule storage: %w", err)
|
||||
}
|
||||
|
||||
engine = urlfilter.NewDNSEngine(rs)
|
||||
log.Info("safesearch: filter %d: reset %d rules", listID, engine.RulesCount)
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// type check
|
||||
var _ filtering.SafeSearch = (*DefaultSafeSearch)(nil)
|
||||
|
||||
// SearchHost implements the [filtering.SafeSearch] interface for *DefaultSafeSearch.
|
||||
func (ss *DefaultSafeSearch) SearchHost(host string, qtype uint16) (res *rules.DNSRewrite) {
|
||||
r, _ := ss.engine.MatchRequest(&urlfilter.DNSRequest{
|
||||
Hostname: strings.ToLower(host),
|
||||
DNSType: qtype,
|
||||
})
|
||||
|
||||
rewritesRules := r.DNSRewrites()
|
||||
if len(rewritesRules) > 0 {
|
||||
return rewritesRules[0].DNSRewrite
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckHost implements the [filtering.SafeSearch] interface for
|
||||
// *DefaultSafeSearch.
|
||||
func (ss *DefaultSafeSearch) CheckHost(
|
||||
host string,
|
||||
qtype uint16,
|
||||
) (res filtering.Result, err error) {
|
||||
if log.GetLevel() >= log.DEBUG {
|
||||
timer := log.StartTimer()
|
||||
defer timer.LogElapsed("safesearch: lookup for %s", host)
|
||||
}
|
||||
|
||||
// Check cache. Return cached result if it was found
|
||||
cachedValue, isFound := ss.getCachedResult(host)
|
||||
if isFound {
|
||||
log.Debug("safesearch: found in cache: %s", host)
|
||||
|
||||
return cachedValue, nil
|
||||
}
|
||||
|
||||
rewrite := ss.SearchHost(host, qtype)
|
||||
if rewrite == nil {
|
||||
return filtering.Result{}, nil
|
||||
}
|
||||
|
||||
dRes, err := ss.newResult(rewrite, qtype)
|
||||
if err != nil {
|
||||
log.Debug("safesearch: failed to lookup addresses for %s: %s", host, err)
|
||||
|
||||
return filtering.Result{}, err
|
||||
}
|
||||
|
||||
if dRes != nil {
|
||||
res = *dRes
|
||||
ss.setCacheResult(host, res)
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return filtering.Result{}, fmt.Errorf("no ipv4 addresses in safe search response for %s", host)
|
||||
}
|
||||
|
||||
// newResult creates Result object from rewrite rule.
|
||||
func (ss *DefaultSafeSearch) newResult(
|
||||
rewrite *rules.DNSRewrite,
|
||||
qtype uint16,
|
||||
) (res *filtering.Result, err error) {
|
||||
res = &filtering.Result{
|
||||
Rules: []*filtering.ResultRule{{
|
||||
FilterListID: filtering.SafeSearchListID,
|
||||
}},
|
||||
Reason: filtering.FilteredSafeSearch,
|
||||
IsFiltered: true,
|
||||
}
|
||||
|
||||
if rewrite.RRType == qtype && (qtype == dns.TypeA || qtype == dns.TypeAAAA) {
|
||||
ip, ok := rewrite.Value.(net.IP)
|
||||
if !ok || ip == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
res.Rules[0].IP = ip
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
if rewrite.NewCNAME == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ips, err := ss.resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip = ip.To4(); ip == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
res.Rules[0].IP = ip
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// setCacheResult stores data in cache for host.
|
||||
func (ss *DefaultSafeSearch) setCacheResult(host string, res filtering.Result) {
|
||||
expire := uint32(time.Now().Add(ss.cacheTime).Unix())
|
||||
exp := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(exp, expire)
|
||||
buf := bytes.NewBuffer(exp)
|
||||
|
||||
err := gob.NewEncoder(buf).Encode(res)
|
||||
if err != nil {
|
||||
log.Error("safesearch: cache encoding: %s", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val := buf.Bytes()
|
||||
_ = ss.safeSearchCache.Set([]byte(host), val)
|
||||
|
||||
log.Debug("safesearch: stored in cache: %s (%d bytes)", host, len(val))
|
||||
}
|
||||
|
||||
// getCachedResult returns stored data from cache for host.
|
||||
func (ss *DefaultSafeSearch) getCachedResult(host string) (res filtering.Result, ok bool) {
|
||||
res = filtering.Result{}
|
||||
|
||||
data := ss.safeSearchCache.Get([]byte(host))
|
||||
if data == nil {
|
||||
return res, false
|
||||
}
|
||||
|
||||
exp := binary.BigEndian.Uint32(data[:4])
|
||||
if exp <= uint32(time.Now().Unix()) {
|
||||
ss.safeSearchCache.Del([]byte(host))
|
||||
|
||||
return res, false
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(data[4:])
|
||||
|
||||
err := gob.NewDecoder(buf).Decode(&res)
|
||||
if err != nil {
|
||||
log.Debug("safesearch: cache decoding: %s", err)
|
||||
|
||||
return filtering.Result{}, false
|
||||
}
|
||||
|
||||
return res, true
|
||||
}
|
||||
202
internal/filtering/safesearch/safesearch_test.go
Normal file
202
internal/filtering/safesearch/safesearch_test.go
Normal file
@@ -0,0 +1,202 @@
|
||||
package safesearch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
||||
"github.com/AdguardTeam/urlfilter/rules"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const (
|
||||
safeSearchCacheSize = 5000
|
||||
cacheTime = 30 * time.Minute
|
||||
)
|
||||
|
||||
var defaultSafeSearchConf = filtering.SafeSearchConfig{
|
||||
Enabled: true,
|
||||
Bing: true,
|
||||
DuckDuckGo: true,
|
||||
Google: true,
|
||||
Pixabay: true,
|
||||
Yandex: true,
|
||||
YouTube: true,
|
||||
}
|
||||
|
||||
var yandexIP = net.IPv4(213, 180, 193, 56)
|
||||
|
||||
func newForTest(t testing.TB, ssConf filtering.SafeSearchConfig) (ss *DefaultSafeSearch) {
|
||||
ss, err := NewDefaultSafeSearch(ssConf, safeSearchCacheSize, cacheTime)
|
||||
require.NoError(t, err)
|
||||
|
||||
return ss
|
||||
}
|
||||
|
||||
func TestSafeSearch(t *testing.T) {
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
val := ss.SearchHost("www.google.com", dns.TypeA)
|
||||
|
||||
assert.Equal(t, &rules.DNSRewrite{NewCNAME: "forcesafesearch.google.com"}, val)
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchYandex(t *testing.T) {
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
"yandex.ru",
|
||||
"yAndeX.ru",
|
||||
"YANdex.COM",
|
||||
"yandex.by",
|
||||
"yandex.kz",
|
||||
"www.yandex.com",
|
||||
} {
|
||||
res, err := ss.CheckHost(host, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, yandexIP, res.Rules[0].IP)
|
||||
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHostSafeSearchGoogle(t *testing.T) {
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ip, _ := resolver.HostToIPs("forcesafesearch.google.com")
|
||||
|
||||
ss := newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
|
||||
// Check host for each domain.
|
||||
for _, host := range []string{
|
||||
"www.google.com",
|
||||
"www.google.im",
|
||||
"www.google.co.in",
|
||||
"www.google.iq",
|
||||
"www.google.is",
|
||||
"www.google.it",
|
||||
"www.google.je",
|
||||
} {
|
||||
t.Run(host, func(t *testing.T) {
|
||||
res, err := ss.CheckHost(host, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.True(t, res.IsFiltered)
|
||||
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, ip, res.Rules[0].IP)
|
||||
assert.EqualValues(t, filtering.SafeSearchListID, res.Rules[0].FilterListID)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheYandex(t *testing.T) {
|
||||
const domain = "yandex.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
// Check host with disabled safesearch.
|
||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
// For yandex we already know valid IP.
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.Equal(t, res.Rules[0].IP, yandexIP)
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.Equal(t, cachedValue.Rules[0].IP, yandexIP)
|
||||
}
|
||||
|
||||
func TestSafeSearchCacheGoogle(t *testing.T) {
|
||||
const domain = "www.google.ru"
|
||||
|
||||
ss := newForTest(t, filtering.SafeSearchConfig{Enabled: false})
|
||||
|
||||
res, err := ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.False(t, res.IsFiltered)
|
||||
assert.Empty(t, res.Rules)
|
||||
|
||||
resolver := &aghtest.TestResolver{}
|
||||
ss = newForTest(t, defaultSafeSearchConf)
|
||||
ss.resolver = resolver
|
||||
|
||||
// Lookup for safesearch domain.
|
||||
rewrite := ss.SearchHost(domain, dns.TypeA)
|
||||
|
||||
ips, err := resolver.LookupIP(context.Background(), "ip", rewrite.NewCNAME)
|
||||
require.NoError(t, err)
|
||||
|
||||
var foundIP net.IP
|
||||
for _, ip := range ips {
|
||||
if ip.To4() != nil {
|
||||
foundIP = ip
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
res, err = ss.CheckHost(domain, dns.TypeA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.Rules, 1)
|
||||
|
||||
assert.True(t, res.Rules[0].IP.Equal(foundIP))
|
||||
|
||||
// Check cache.
|
||||
cachedValue, isFound := ss.getCachedResult(domain)
|
||||
require.True(t, isFound)
|
||||
require.Len(t, cachedValue.Rules, 1)
|
||||
|
||||
assert.True(t, cachedValue.Rules[0].IP.Equal(foundIP))
|
||||
}
|
||||
|
||||
const googleHost = "www.google.com"
|
||||
|
||||
var dnsRewriteSink *rules.DNSRewrite
|
||||
|
||||
func BenchmarkSafeSearch(b *testing.B) {
|
||||
ss := newForTest(b, defaultSafeSearchConf)
|
||||
|
||||
for n := 0; n < b.N; n++ {
|
||||
dnsRewriteSink = ss.SearchHost(googleHost, dns.TypeA)
|
||||
}
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteSink.NewCNAME)
|
||||
}
|
||||
|
||||
var dnsRewriteParallelSink *rules.DNSRewrite
|
||||
|
||||
func BenchmarkSafeSearch_parallel(b *testing.B) {
|
||||
ss := newForTest(b, defaultSafeSearchConf)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
dnsRewriteParallelSink = ss.SearchHost(googleHost, dns.TypeA)
|
||||
}
|
||||
})
|
||||
|
||||
assert.Equal(b, "forcesafesearch.google.com", dnsRewriteParallelSink.NewCNAME)
|
||||
}
|
||||
@@ -1370,6 +1370,7 @@ var blockedServices = []blockedService{{
|
||||
"||mastodon.au^",
|
||||
"||mastodon.bida.im^",
|
||||
"||mastodon.com.tr^",
|
||||
"||mastodon.eus^",
|
||||
"||mastodon.green^",
|
||||
"||mastodon.ie^",
|
||||
"||mastodon.iriseden.eu^",
|
||||
@@ -1397,7 +1398,6 @@ var blockedServices = []blockedService{{
|
||||
"||mindly.social^",
|
||||
"||mstdn.ca^",
|
||||
"||mstdn.jp^",
|
||||
"||mstdn.party^",
|
||||
"||mstdn.social^",
|
||||
"||muenchen.social^",
|
||||
"||newsie.social^",
|
||||
@@ -1435,11 +1435,11 @@ var blockedServices = []blockedService{{
|
||||
"||toot.wales^",
|
||||
"||troet.cafe^",
|
||||
"||twingyeo.kr^",
|
||||
"||uiuxdev.social^",
|
||||
"||union.place^",
|
||||
"||universeodon.com^",
|
||||
"||urbanists.social^",
|
||||
"||vocalodon.net^",
|
||||
"||wien.rocks^",
|
||||
"||wxw.moe^",
|
||||
},
|
||||
}, {
|
||||
@@ -1636,6 +1636,14 @@ var blockedServices = []blockedService{{
|
||||
"||snapchat.com^",
|
||||
"||snapkit.co",
|
||||
},
|
||||
}, {
|
||||
ID: "soundcloud",
|
||||
Name: "SoundCloud",
|
||||
IconSVG: []byte("<svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"currentColor\" viewBox=\"0 0 24 24\" fill-rule=\"evenodd\" clip-rule=\"evenodd\"><path d=\"M19 17.75c2.07 0 3.75-1.68 3.75-3.75 0-2.07-1.68-3.75-3.75-3.75-.173 0-.344.012-.511.035-.73-2.337-2.913-4.035-5.489-4.035-.818 0-1.596.171-2.301.48-.273.119-.449.389-.449.687l0 9.583c0 .414.336.75.75.75l8 0zM7.25 8l0 9c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-9c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75zM4.25 10l0 7c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-7c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75zM1.25 12l0 5c0 .414.336.75.75.75.414 0 .75-.336.75-.75l0-5c0-.414-.336-.75-.75-.75-.414 0-.75.336-.75.75z\"/></svg>"),
|
||||
Rules: []string{
|
||||
"||sndcdn.com^",
|
||||
"||soundcloud.com^",
|
||||
},
|
||||
}, {
|
||||
ID: "spotify",
|
||||
Name: "Spotify",
|
||||
|
||||
@@ -285,6 +285,12 @@ var config = &configuration{
|
||||
TrustedProxies: []string{"127.0.0.0/8", "::1/128"},
|
||||
CacheSize: 4 * 1024 * 1024,
|
||||
|
||||
EDNSClientSubnet: &dnsforward.EDNSClientSubnet{
|
||||
CustomIP: "",
|
||||
Enabled: false,
|
||||
UseCustom: false,
|
||||
},
|
||||
|
||||
// set default maximum concurrent queries to 300
|
||||
// we introduced a default limit due to this:
|
||||
// https://github.com/AdguardTeam/AdGuardHome/issues/2015#issuecomment-674041912
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// currentSchemaVersion is the current schema version.
|
||||
const currentSchemaVersion = 16
|
||||
const currentSchemaVersion = 17
|
||||
|
||||
// These aliases are provided for convenience.
|
||||
type (
|
||||
@@ -89,6 +89,7 @@ func upgradeConfigSchema(oldVersion int, diskConf yobj) (err error) {
|
||||
upgradeSchema13to14,
|
||||
upgradeSchema14to15,
|
||||
upgradeSchema15to16,
|
||||
upgradeSchema16to17,
|
||||
}
|
||||
|
||||
n := 0
|
||||
@@ -892,19 +893,56 @@ func upgradeSchema15to16(diskConf yobj) (err error) {
|
||||
"ignored": []any{},
|
||||
}
|
||||
|
||||
k := "statistics_interval"
|
||||
v, has := dns[k]
|
||||
const field = "statistics_interval"
|
||||
v, has := dns[field]
|
||||
if has {
|
||||
stats["enabled"] = v != 0
|
||||
stats["interval"] = v
|
||||
}
|
||||
delete(dns, k)
|
||||
delete(dns, field)
|
||||
|
||||
diskConf["statistics"] = stats
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// upgradeSchema16to17 performs the following changes:
|
||||
//
|
||||
// # BEFORE:
|
||||
// 'dns':
|
||||
// 'edns_client_subnet': false
|
||||
//
|
||||
// # AFTER:
|
||||
// 'dns':
|
||||
// 'edns_client_subnet':
|
||||
// 'enabled': false
|
||||
// 'use_custom': false
|
||||
// 'custom_ip': ""
|
||||
func upgradeSchema16to17(diskConf yobj) (err error) {
|
||||
log.Printf("Upgrade yaml: 16 to 17")
|
||||
diskConf["schema_version"] = 17
|
||||
|
||||
dnsVal, ok := diskConf["dns"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
dns, ok := dnsVal.(yobj)
|
||||
if !ok {
|
||||
return fmt.Errorf("unexpected type of dns: %T", dnsVal)
|
||||
}
|
||||
|
||||
const field = "edns_client_subnet"
|
||||
|
||||
dns[field] = map[string]any{
|
||||
"enabled": dns[field] == true,
|
||||
"use_custom": false,
|
||||
"custom_ip": "",
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(a.garipov): Replace with log.Output when we port it to our logging
|
||||
// package.
|
||||
func funcName() string {
|
||||
|
||||
@@ -747,3 +747,64 @@ func TestUpgradeSchema15to16(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpgradeSchema16to17(t *testing.T) {
|
||||
const newSchemaVer = 17
|
||||
|
||||
defaultWantObj := yobj{
|
||||
"dns": map[string]any{
|
||||
"edns_client_subnet": map[string]any{
|
||||
"enabled": false,
|
||||
"use_custom": false,
|
||||
"custom_ip": "",
|
||||
},
|
||||
},
|
||||
"schema_version": newSchemaVer,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
in yobj
|
||||
want yobj
|
||||
name string
|
||||
}{{
|
||||
in: yobj{
|
||||
"dns": map[string]any{
|
||||
"edns_client_subnet": false,
|
||||
},
|
||||
},
|
||||
want: defaultWantObj,
|
||||
name: "basic",
|
||||
}, {
|
||||
in: yobj{
|
||||
"dns": map[string]any{},
|
||||
},
|
||||
want: defaultWantObj,
|
||||
name: "default_values",
|
||||
}, {
|
||||
in: yobj{
|
||||
"dns": map[string]any{
|
||||
"edns_client_subnet": true,
|
||||
},
|
||||
},
|
||||
want: yobj{
|
||||
"dns": map[string]any{
|
||||
"edns_client_subnet": map[string]any{
|
||||
"enabled": true,
|
||||
"use_custom": false,
|
||||
"custom_ip": "",
|
||||
},
|
||||
},
|
||||
"schema_version": newSchemaVer,
|
||||
},
|
||||
name: "is_true",
|
||||
}}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := upgradeSchema16to17(tc.in)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, tc.want, tc.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
// 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 {
|
||||
proxy *proxy.Proxy
|
||||
bootstraps []string
|
||||
upstreams []string
|
||||
upsTimeout time.Duration
|
||||
running atomic.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, 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.
|
||||
svc.running.Store(err == nil)
|
||||
}()
|
||||
|
||||
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 svc.running.Load() {
|
||||
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
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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),
|
||||
})
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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),
|
||||
}
|
||||
|
||||
var started atomic.Bool
|
||||
confMgr := newConfigManager()
|
||||
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||
OnStart: func() (err error) {
|
||||
started.Store(true)
|
||||
|
||||
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.True(t, started.Load())
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
assert.Equal(t, wantDNS, resp)
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
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"
|
||||
)
|
||||
@@ -1,42 +0,0 @@
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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(),
|
||||
})
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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))
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
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) {
|
||||
var accepted atomic.Bool
|
||||
var l net.Listener = &aghtest.Listener{
|
||||
OnAccept: func() (conn net.Conn, err error) {
|
||||
accepted.Store(true)
|
||||
|
||||
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.True(t, accepted.Load())
|
||||
}
|
||||
@@ -1,305 +0,0 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package websvc
|
||||
|
||||
import "time"
|
||||
|
||||
// testTimeout is the common timeout for tests.
|
||||
const testTimeout = 1 * time.Second
|
||||
@@ -1,187 +0,0 @@
|
||||
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)
|
||||
}
|
||||
20
main_next.go
20
main_next.go
@@ -1,20 +0,0 @@
|
||||
//go: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 build
|
||||
var clientBuildFS embed.FS
|
||||
|
||||
func main() {
|
||||
cmd.Main(clientBuildFS)
|
||||
}
|
||||
5041
openapi/v1.yaml
5041
openapi/v1.yaml
File diff suppressed because it is too large
Load Diff
@@ -128,13 +128,7 @@ export CGO_ENABLED
|
||||
GO111MODULE='on'
|
||||
export GO111MODULE
|
||||
|
||||
# Build the new binary if requested.
|
||||
if [ "${NEXTAPI:-0}" -eq '0' ]
|
||||
then
|
||||
tags_flags='--tags='
|
||||
else
|
||||
tags_flags='--tags=next'
|
||||
fi
|
||||
tags_flags='--tags='
|
||||
readonly tags_flags
|
||||
|
||||
if [ "$verbose" -gt '0' ]
|
||||
|
||||
@@ -35,7 +35,7 @@ set -f -u
|
||||
go_version="$( "${GO:-go}" version )"
|
||||
readonly go_version
|
||||
|
||||
go_min_version='go1.19.6'
|
||||
go_min_version='go1.19.7'
|
||||
go_version_msg="
|
||||
warning: your go version (${go_version}) is different from the recommended minimal one (${go_min_version}).
|
||||
if you have the version installed, please set the GO environment variable.
|
||||
@@ -122,7 +122,6 @@ underscores() {
|
||||
-e '_freebsd.go'\
|
||||
-e '_linux.go'\
|
||||
-e '_little.go'\
|
||||
-e '_next.go'\
|
||||
-e '_openbsd.go'\
|
||||
-e '_others.go'\
|
||||
-e '_test.go'\
|
||||
@@ -178,7 +177,6 @@ run_linter gocyclo --over 10\
|
||||
./internal/stats/\
|
||||
./internal/tools/\
|
||||
./internal/updater/\
|
||||
./internal/next/\
|
||||
./internal/version/\
|
||||
./scripts/blocked-services/\
|
||||
./scripts/vetted-filters/\
|
||||
|
||||
Reference in New Issue
Block a user