Merge branch 'master' into 4990-custom-ciphers
This commit is contained in:
commit
a736f67205
|
@ -1,7 +1,7 @@
|
||||||
'name': 'build'
|
'name': 'build'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.6'
|
'GO_VERSION': '1.18.7'
|
||||||
'NODE_VERSION': '14'
|
'NODE_VERSION': '14'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'name': 'lint'
|
'name': 'lint'
|
||||||
|
|
||||||
'env':
|
'env':
|
||||||
'GO_VERSION': '1.18.6'
|
'GO_VERSION': '1.18.7'
|
||||||
|
|
||||||
'on':
|
'on':
|
||||||
'push':
|
'push':
|
||||||
|
|
95
CHANGELOG.md
95
CHANGELOG.md
|
@ -11,30 +11,80 @@ and this project adheres to
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- The new optional `tls.override_tls_ciphers` property list, which can be set in
|
|
||||||
the configuration file. It allows overriding TLS Ciphers that are used for
|
|
||||||
https listeners ([#4925])
|
|
||||||
|
|
||||||
[#4925]: https://github.com/AdguardTeam/AdGuardHome/issues/4925
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## [v0.108.0] - TBA (APPROX.)
|
## [v0.108.0] - TBA (APPROX.)
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
## Added
|
||||||
|
|
||||||
|
- The new optional `tls.override_tls_ciphers` property, which allows
|
||||||
|
overriding TLS ciphers used by AdGuard Home ([#4925], [#4990]).
|
||||||
|
- The ability to serve DNS on link-local IPv6 addresses ([#2926]).
|
||||||
|
- The ability to put [ClientIDs][clientid] into DNS-over-HTTPS hostnames as
|
||||||
|
opposed to URL paths ([#3418]). Note that AdGuard Home checks the server name
|
||||||
|
only if the URL does not contain a ClientID.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Responses with `SERVFAIL` code are now cached for at least 30 seconds.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The default value of `dns.cache_size` accidentally set to 0 has now been
|
||||||
|
reverted to 4 MiB ([#5010]).
|
||||||
|
- Responses for which the DNSSEC validation had explicitly been omitted aren't
|
||||||
|
cached now ([#4942]).
|
||||||
|
- Web UI not switching to HTTP/3 ([#4986], [#4993]).
|
||||||
|
|
||||||
|
[#2926]: https://github.com/AdguardTeam/AdGuardHome/issues/2926
|
||||||
|
[#3418]: https://github.com/AdguardTeam/AdGuardHome/issues/3418
|
||||||
|
[#4925]: https://github.com/AdguardTeam/AdGuardHome/issues/4925
|
||||||
|
[#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942
|
||||||
|
[#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986
|
||||||
|
[#4990]: https://github.com/AdguardTeam/AdGuardHome/issues/4990
|
||||||
|
[#4993]: https://github.com/AdguardTeam/AdGuardHome/issues/4993
|
||||||
|
[#5010]: https://github.com/AdguardTeam/AdGuardHome/issues/5010
|
||||||
|
|
||||||
|
[clientid]: https://github.com/AdguardTeam/AdGuardHome/wiki/Clients#clientid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
## [v0.107.16] - 2022-11-02 (APPROX.)
|
## [v0.107.17] - 2022-11-02 (APPROX.)
|
||||||
|
|
||||||
See also the [v0.107.16 GitHub milestone][ms-v0.107.15].
|
See also the [v0.107.17 GitHub milestone][ms-v0.107.17].
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
[ms-v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
|
[ms-v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
|
||||||
|
||||||| bf792b83f
|
||||||
|
[ms-v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
|
||||||
|
=======
|
||||||
|
[ms-v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/milestone/52?closed=1
|
||||||
|
>>>>>>> master
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
||||||| bf792b83f
|
||||||
|
|
||||||
|
=======
|
||||||
|
|
||||||
|
## [v0.107.16] - 2022-10-07
|
||||||
|
|
||||||
|
This is a security update. There is no GitHub milestone, since no GitHub issues
|
||||||
|
were resolved.
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
|
CVE-2022-2879, CVE-2022-2880, and CVE-2022-41715 Go vulnerabilities fixed in
|
||||||
|
[Go 1.18.7][go-1.18.7].
|
||||||
|
|
||||||
|
[go-1.18.7]: https://groups.google.com/g/golang-announce/c/xtuG5faxtaU
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
>>>>>>> master
|
||||||
## [v0.107.15] - 2022-10-03
|
## [v0.107.15] - 2022-10-03
|
||||||
|
|
||||||
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
See also the [v0.107.15 GitHub milestone][ms-v0.107.15].
|
||||||
|
@ -57,7 +107,7 @@ experimental and may break or change in the future.
|
||||||
explicitly enabled by setting the new property `dns.serve_http3` in the
|
explicitly enabled by setting the new property `dns.serve_http3` in the
|
||||||
configuration file to `true`.
|
configuration file to `true`.
|
||||||
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
|
- DNS-over-HTTP upstreams can now upgrade to HTTP/3 if the new configuration
|
||||||
file property `use_http3_upstreams` is set to `true`.
|
file property `dns.use_http3_upstreams` is set to `true`.
|
||||||
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
|
- Upstreams with forced DNS-over-HTTP/3 and no fallback to prior HTTP versions
|
||||||
using the `h3://` scheme.
|
using the `h3://` scheme.
|
||||||
|
|
||||||
|
@ -171,7 +221,7 @@ See also the [v0.107.12 GitHub milestone][ms-v0.107.12].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in
|
CVE-2022-27664 and CVE-2022-32190 Go vulnerabilities fixed in
|
||||||
[Go 1.18.6][go-1.18.6].
|
[Go 1.18.6][go-1.18.6].
|
||||||
|
|
||||||
|
@ -292,7 +342,7 @@ See also the [v0.107.9 GitHub milestone][ms-v0.107.9].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
CVE-2022-32189 Go vulnerability fixed in [Go 1.18.5][go-1.18.5]. Go 1.17
|
CVE-2022-32189 Go vulnerability fixed in [Go 1.18.5][go-1.18.5]. Go 1.17
|
||||||
support has also been removed, as it has reached end of life and will not
|
support has also been removed, as it has reached end of life and will not
|
||||||
receive security updates.
|
receive security updates.
|
||||||
|
@ -335,7 +385,7 @@ See also the [v0.107.8 GitHub milestone][ms-v0.107.8].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities
|
CVE-2022-1705, CVE-2022-32148, CVE-2022-30631, and other Go vulnerabilities
|
||||||
fixed in [Go 1.17.12][go-1.17.12].
|
fixed in [Go 1.17.12][go-1.17.12].
|
||||||
|
|
||||||
|
@ -371,7 +421,7 @@ See also the [v0.107.7 GitHub milestone][ms-v0.107.7].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
|
[CVE-2022-29526], [CVE-2022-30634], [CVE-2022-30629], [CVE-2022-30580], and
|
||||||
[CVE-2022-29804] Go vulnerabilities.
|
[CVE-2022-29804] Go vulnerabilities.
|
||||||
- Enforced password strength policy ([#3503]).
|
- Enforced password strength policy ([#3503]).
|
||||||
|
@ -528,7 +578,7 @@ See also the [v0.107.6 GitHub milestone][ms-v0.107.6].
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
|
- `User-Agent` HTTP header removed from outgoing DNS-over-HTTPS requests.
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.
|
[CVE-2022-24675], [CVE-2022-27536], and [CVE-2022-28327] Go vulnerabilities.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -583,7 +633,7 @@ were resolved.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-24921] Go vulnerability.
|
[CVE-2022-24921] Go vulnerability.
|
||||||
|
|
||||||
[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921
|
[CVE-2022-24921]: https://www.cvedetails.com/cve/CVE-2022-24921
|
||||||
|
@ -596,7 +646,7 @@ See also the [v0.107.4 GitHub milestone][ms-v0.107.4].
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- Go version was updated to prevent the possibility of exploiting the
|
- Go version has been updated to prevent the possibility of exploiting the
|
||||||
[CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.
|
[CVE-2022-23806], [CVE-2022-23772], and [CVE-2022-23773] Go vulnerabilities.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
@ -1333,11 +1383,12 @@ See also the [v0.104.2 GitHub milestone][ms-v0.104.2].
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.17...HEAD
|
||||||
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.15
|
[v0.107.17]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...v0.107.17
|
||||||
-->
|
-->
|
||||||
|
|
||||||
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...HEAD
|
[Unreleased]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.16...HEAD
|
||||||
|
[v0.107.16]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.15...v0.107.16
|
||||||
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
|
[v0.107.15]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.14...v0.107.15
|
||||||
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
[v0.107.14]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.13...v0.107.14
|
||||||
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
|
[v0.107.13]: https://github.com/AdguardTeam/AdGuardHome/compare/v0.107.12...v0.107.13
|
||||||
|
|
4
Makefile
4
Makefile
|
@ -34,7 +34,7 @@ YARN_INSTALL_FLAGS = $(YARN_FLAGS) --network-timeout 120000 --silent\
|
||||||
--ignore-engines --ignore-optional --ignore-platform\
|
--ignore-engines --ignore-optional --ignore-platform\
|
||||||
--ignore-scripts
|
--ignore-scripts
|
||||||
|
|
||||||
V1API = 0
|
NEXTAPI = 0
|
||||||
|
|
||||||
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
# Macros for the build-release target. If FRONTEND_PREBUILT is 0, the
|
||||||
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
# default, the macro $(BUILD_RELEASE_DEPS_$(FRONTEND_PREBUILT)) expands
|
||||||
|
@ -63,7 +63,7 @@ ENV = env\
|
||||||
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
PATH="$${PWD}/bin:$$( "$(GO.MACRO)" env GOPATH )/bin:$${PATH}"\
|
||||||
RACE='$(RACE)'\
|
RACE='$(RACE)'\
|
||||||
SIGN='$(SIGN)'\
|
SIGN='$(SIGN)'\
|
||||||
V1API='$(V1API)'\
|
NEXTAPI='$(NEXTAPI)'\
|
||||||
VERBOSE='$(VERBOSE)'\
|
VERBOSE='$(VERBOSE)'\
|
||||||
VERSION='$(VERSION)'\
|
VERSION='$(VERSION)'\
|
||||||
|
|
||||||
|
|
555
README.md
555
README.md
|
@ -10,68 +10,76 @@
|
||||||
Free and open source, powerful network-wide ads & trackers blocking DNS
|
Free and open source, powerful network-wide ads & trackers blocking DNS
|
||||||
server.
|
server.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://adguard.com/">AdGuard.com</a> |
|
<a href="https://adguard.com/">AdGuard.com</a> |
|
||||||
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
|
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
|
||||||
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
<a href="https://reddit.com/r/Adguard">Reddit</a> |
|
||||||
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
||||||
<a href="https://t.me/adguard_en">Telegram</a>
|
<a href="https://t.me/adguard_en">Telegram</a>
|
||||||
<br /><br />
|
<br/><br/>
|
||||||
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
||||||
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage" />
|
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
|
<a href="https://goreportcard.com/report/AdguardTeam/AdGuardHome">
|
||||||
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card" />
|
<img src="https://goreportcard.com/badge/github.com/AdguardTeam/AdGuardHome" alt="Go Report Card"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://hub.docker.com/r/adguard/adguardhome">
|
<a href="https://hub.docker.com/r/adguard/adguardhome">
|
||||||
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800" />
|
<img alt="Docker Pulls" src="https://img.shields.io/docker/pulls/adguard/adguardhome.svg?maxAge=604800"/>
|
||||||
</a>
|
</a>
|
||||||
<br />
|
<br/>
|
||||||
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
|
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
|
||||||
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
|
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release"/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://snapcraft.io/adguard-home">
|
<a href="https://snapcraft.io/adguard-home">
|
||||||
<img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg" />
|
<img alt="adguard-home" src="https://snapcraft.io/adguard-home/badge.svg"/>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<br/>
|
||||||
<br />
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://cdn.adtidy.org/public/Adguard/Common/adguard_home.gif" width="800" />
|
<img src="https://cdn.adtidy.org/public/Adguard/Common/adguard_home.gif" width="800"/>
|
||||||
</p>
|
</p>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
<hr />
|
AdGuard Home is a network-wide software for blocking ads and tracking. After you
|
||||||
|
set it up, it'll cover ALL your home devices, and you don't need any client-side
|
||||||
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
|
software for that.
|
||||||
|
|
||||||
It operates as a DNS server that re-routes tracking domains to a “black hole”,
|
It operates as a DNS server that re-routes tracking domains to a “black hole”,
|
||||||
thus preventing your devices from connecting to those servers. It's based on
|
thus preventing your devices from connecting to those servers. It's based on
|
||||||
software we use for our public [AdGuard DNS](https://adguard-dns.io/) servers,
|
software we use for our public [AdGuard DNS] servers, and both share a lot of
|
||||||
and both share a lot of code.
|
code.
|
||||||
|
|
||||||
|
[AdGuard DNS]: https://adguard-dns.io/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
* [Getting Started](#getting-started)
|
* [Getting Started](#getting-started)
|
||||||
* [Comparing AdGuard Home to other solutions](#comparison)
|
* [Automated install (Unix)](#automated-install-linux-and-mac)
|
||||||
* [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
|
* [Alternative methods](#alternative-methods)
|
||||||
* [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
|
* [Guides](#guides)
|
||||||
* [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
|
* [API](#api)
|
||||||
* [How to build from source](#how-to-build)
|
* [Comparing AdGuard Home to other solutions](#comparison)
|
||||||
* [Contributing](#contributing)
|
* [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
|
||||||
* [Test unstable versions](#test-unstable-versions)
|
* [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
|
||||||
* [Reporting issues](#reporting-issues)
|
* [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
|
||||||
* [Help with translations](#translate)
|
* [Known limitations](#comparison-limitations)
|
||||||
* [Other](#help-other)
|
* [How to build from source](#how-to-build)
|
||||||
* [Projects that use AdGuard Home](#uses)
|
* [Prerequisites](#prerequisites)
|
||||||
* [Acknowledgments](#acknowledgments)
|
* [Building](#building)
|
||||||
* [Privacy](#privacy)
|
* [Contributing](#contributing)
|
||||||
|
* [Test unstable versions](#test-unstable-versions)
|
||||||
|
* [Reporting issues](#reporting-issues)
|
||||||
|
* [Help with translations](#translate)
|
||||||
|
* [Other](#help-other)
|
||||||
|
* [Projects that use AdGuard Home](#uses)
|
||||||
|
* [Acknowledgments](#acknowledgments)
|
||||||
|
* [Privacy](#privacy)
|
||||||
|
|
||||||
<a id="getting-started"></a>
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Automated install (Linux and Mac)
|
|
||||||
|
## <a href="#getting-started" id="getting-started" name="getting-started">Getting Started</a>
|
||||||
|
|
||||||
|
### <a href="#automated-install-linux-and-mac" id="automated-install-linux-and-mac" name="automated-install-linux-and-mac">Automated install (Unix)</a>
|
||||||
|
|
||||||
Run the following command in your terminal:
|
Run the following command in your terminal:
|
||||||
|
|
||||||
|
@ -80,73 +88,96 @@ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/s
|
||||||
```
|
```
|
||||||
|
|
||||||
The script also accepts some options:
|
The script also accepts some options:
|
||||||
* `-c <channel>` to use specified channel.
|
|
||||||
* `-r` to reinstall AdGuard Home;
|
* `-c <channel>` to use specified channel;
|
||||||
* `-u` to uninstall AdGuard Home;
|
* `-r` to reinstall AdGuard Home;
|
||||||
* `-v` for verbose output;
|
* `-u` to uninstall AdGuard Home;
|
||||||
|
* `-v` for verbose output.
|
||||||
|
|
||||||
Note that options `-r` and `-u` are mutually exclusive.
|
Note that options `-r` and `-u` are mutually exclusive.
|
||||||
|
|
||||||
### Alternative methods
|
|
||||||
|
|
||||||
#### Manual installation
|
|
||||||
|
|
||||||
Please read the **[Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)** article on our Wiki to learn how to install AdGuard Home manually, and how to configure your devices to use it.
|
### <a href="#alternative-methods" id="alternative-methods" name="alternative-methods">Alternative methods</a>
|
||||||
|
|
||||||
#### Docker
|
#### <a href="#manual-installation" id="manual-installation" name="manual-installation">Manual installation</a>
|
||||||
|
|
||||||
You can use our [official Docker image](https://hub.docker.com/r/adguard/adguardhome).
|
Please read the **[Getting Started][wiki-start]** article on our Wiki to learn
|
||||||
|
how to install AdGuard Home manually, and how to configure your devices to use
|
||||||
|
it.
|
||||||
|
|
||||||
#### Snap Store
|
#### <a href="#docker" id="docker" name="docker">Docker</a>
|
||||||
|
|
||||||
If you're running **Linux**, there's a secure and easy way to install AdGuard Home - you can get it from the [Snap Store](https://snapcraft.io/adguard-home).
|
You can use our official Docker image on [Docker Hub].
|
||||||
|
|
||||||
### Guides
|
#### <a href="#snap-store" id="snap-store" name="snap-store">Snap Store</a>
|
||||||
|
|
||||||
* [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)
|
If you're running **Linux,** there's a secure and easy way to install AdGuard
|
||||||
* [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ)
|
Home: get it from the [Snap Store].
|
||||||
* [How to Write Hosts Blocklists](https://github.com/AdguardTeam/AdGuardHome/wiki/Hosts-Blocklists)
|
|
||||||
* [Comparing AdGuard Home to Other Solutions](https://github.com/AdguardTeam/AdGuardHome/wiki/Comparison)
|
|
||||||
* Configuring AdGuard
|
|
||||||
* [Configuration](https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration)
|
|
||||||
* [Configuring AdGuard Home Clients](https://github.com/AdguardTeam/AdGuardHome/wiki/Clients)
|
|
||||||
* [AdGuard Home as a DoH, DoT, or DoQ Server](https://github.com/AdguardTeam/AdGuardHome/wiki/Encryption)
|
|
||||||
* [AdGuard Home as a DNSCrypt Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DNSCrypt)
|
|
||||||
* [AdGuard Home as a DHCP Server](https://github.com/AdguardTeam/AdGuardHome/wiki/DHCP)
|
|
||||||
* Installing AdGuard Home
|
|
||||||
* [Docker](https://github.com/AdguardTeam/AdGuardHome/wiki/Docker)
|
|
||||||
* [How to Install and Run AdGuard Home on a Raspberry Pi](https://github.com/AdguardTeam/AdGuardHome/wiki/Raspberry-Pi)
|
|
||||||
* [How to Install and Run AdGuard Home on a Virtual Private Server](https://github.com/AdguardTeam/AdGuardHome/wiki/VPS)
|
|
||||||
* [Verifying Releases](https://github.com/AdguardTeam/AdGuardHome/wiki/Verify-Releases)
|
|
||||||
|
|
||||||
### API
|
[Docker Hub]: https://hub.docker.com/r/adguard/adguardhome
|
||||||
|
[Snap Store]: https://snapcraft.io/adguard-home
|
||||||
|
[wiki-start]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started
|
||||||
|
|
||||||
If you want to integrate with AdGuard Home, you can use our [REST API](https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi).
|
|
||||||
Alternatively, you can use this [python client](https://pypi.org/project/adguardhome/), which is used to build the [AdGuard Home Hass.io Add-on](https://www.home-assistant.io/integrations/adguard/).
|
|
||||||
|
|
||||||
<a id="comparison"></a>
|
|
||||||
## Comparing AdGuard Home to other solutions
|
|
||||||
|
|
||||||
<a id="comparison-adguard-dns"></a>
|
### <a href="#guides" id="guides" name="guides">Guides</a>
|
||||||
### How is this different from public AdGuard DNS servers?
|
|
||||||
|
|
||||||
Running your own AdGuard Home server allows you to do much more than using a public DNS server. It's a completely different level. See for yourself:
|
See our [Wiki][wiki].
|
||||||
|
|
||||||
* Choose what exactly the server blocks and permits.
|
[wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki
|
||||||
* Monitor your network activity.
|
|
||||||
* Add your own custom filtering rules.
|
|
||||||
* **Most importantly, this is your own server, and you are the only one who's in control.**
|
|
||||||
|
|
||||||
<a id="comparison-pi-hole"></a>
|
|
||||||
### How does AdGuard Home compare to Pi-Hole
|
|
||||||
|
|
||||||
At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads and trackers using "DNS sinkholing" method, and both allow customizing what's blocked.
|
|
||||||
|
|
||||||
> We're not going to stop here. DNS sinkholing is not a bad starting point, but this is just the beginning.
|
### <a href="#api" id="api" name="api">API</a>
|
||||||
|
|
||||||
AdGuard Home provides a lot of features out-of-the-box with no need to install and configure additional software. We want it to be simple to the point when even casual users can set it up with minimal effort.
|
If you want to integrate with AdGuard Home, you can use our [REST API][openapi].
|
||||||
|
Alternatively, you can use this [python client][pyclient], which is used to
|
||||||
|
build the [AdGuard Home Hass.io Add-on][hassio].
|
||||||
|
|
||||||
> Disclaimer: some of the listed features can be added to Pi-Hole by installing additional software or by manually using SSH terminal and reconfiguring one of the utilities Pi-Hole consists of. However, in our opinion, this cannot be legitimately counted as a Pi-Hole's feature.
|
[hassio]: https://www.home-assistant.io/integrations/adguard/
|
||||||
|
[openapi]: https://github.com/AdguardTeam/AdGuardHome/tree/master/openapi
|
||||||
|
[pyclient]: https://pypi.org/project/adguardhome/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a href="#comparison" id="comparison" name="comparison">Comparing AdGuard Home to other solutions</a>
|
||||||
|
|
||||||
|
### <a href="#comparison-adguard-dns" id="comparison-adguard-dns" name="comparison-adguard-dns">How is this different from public AdGuard DNS servers?</a>
|
||||||
|
|
||||||
|
Running your own AdGuard Home server allows you to do much more than using a
|
||||||
|
public DNS server. It's a completely different level. See for yourself:
|
||||||
|
|
||||||
|
* Choose what exactly the server blocks and permits.
|
||||||
|
|
||||||
|
* Monitor your network activity.
|
||||||
|
|
||||||
|
* Add your own custom filtering rules.
|
||||||
|
|
||||||
|
* **Most importantly, it's your own server, and you are the only one who's in
|
||||||
|
control.**
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a href="#comparison-pi-hole" id="comparison-pi-hole" name="comparison-pi-hole">How does AdGuard Home compare to Pi-Hole</a>
|
||||||
|
|
||||||
|
At this point, AdGuard Home has a lot in common with Pi-Hole. Both block ads
|
||||||
|
and trackers using the so-called “DNS sinkholing” method and both allow
|
||||||
|
customizing what's blocked.
|
||||||
|
|
||||||
|
<aside>
|
||||||
|
We're not going to stop here. DNS sinkholing is not a bad starting point, but
|
||||||
|
this is just the beginning.
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
AdGuard Home provides a lot of features out-of-the-box with no need to install
|
||||||
|
and configure additional software. We want it to be simple to the point when
|
||||||
|
even casual users can set it up with minimal effort.
|
||||||
|
|
||||||
|
**Disclaimer:** some of the listed features can be added to Pi-Hole by
|
||||||
|
installing additional software or by manually using SSH terminal and
|
||||||
|
reconfiguring one of the utilities Pi-Hole consists of. However, in our
|
||||||
|
opinion, this cannot be legitimately counted as a Pi-Hole's feature.
|
||||||
|
|
||||||
| Feature | AdGuard Home | Pi-Hole |
|
| Feature | AdGuard Home | Pi-Hole |
|
||||||
|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
|
|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
|
||||||
|
@ -162,53 +193,72 @@ AdGuard Home provides a lot of features out-of-the-box with no need to install a
|
||||||
| Force Safe search on search engines | ✅ | ❌ |
|
| Force Safe search on search engines | ✅ | ❌ |
|
||||||
| Per-client (device) configuration | ✅ | ✅ |
|
| Per-client (device) configuration | ✅ | ✅ |
|
||||||
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
| Access settings (choose who can use AGH DNS) | ✅ | ❌ |
|
||||||
| Running [without root privileges](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser) | ✅ | ❌ |
|
| Running [without root privileges][wiki-noroot] | ✅ | ❌ |
|
||||||
|
|
||||||
<a id="comparison-adblock"></a>
|
[wiki-noroot]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser
|
||||||
### How does AdGuard Home compare to traditional ad blockers
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a href="#comparison-adblock" id="comparison-adblock" name="comparison-adblock">How does AdGuard Home compare to traditional ad blockers</a>
|
||||||
|
|
||||||
It depends.
|
It depends.
|
||||||
|
|
||||||
“DNS sinkholing” is capable of blocking a big percentage of ads, but it lacks
|
DNS sinkholing is capable of blocking a big percentage of ads, but it lacks
|
||||||
flexibility and power of traditional ad blockers. You can get a good impression
|
the flexibility and the power of traditional ad blockers. You can get a good
|
||||||
about the difference between these methods by reading
|
impression about the difference between these methods by reading [this
|
||||||
[this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It
|
article][blog-adaway], which compares AdGuard for Android (a traditional ad
|
||||||
compares AdGuard for Android (a traditional ad blocker) to hosts-level ad
|
blocker) to hosts-level ad blockers (which are almost identical to DNS-based
|
||||||
blockers (which are almost identical to DNS-based blockers in their
|
blockers in their capabilities). This level of protection is enough for some
|
||||||
capabilities). This level of protection is enough for some users.
|
users.
|
||||||
|
|
||||||
|
Additionally, using a DNS-based blocker can help to block ads, tracking and
|
||||||
|
analytics requests on other types of devices, such as SmartTVs, smart speakers
|
||||||
|
or other kinds of IoT devices (on which you can't install traditional ad
|
||||||
|
blockers).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Additionally, using a DNS-based blocker can help to block ads, tracking and analytics requests on other types of devices, such as SmartTVs, smart speakers or other kinds of IoT devices (on which you can't install traditional ad blockers).
|
### <a href="#comparison-limitations" id="comparison-limitations" name="comparison-limitations">Known limitations</a>
|
||||||
|
|
||||||
**Known limitations**
|
|
||||||
|
|
||||||
Here are some examples of what cannot be blocked by a DNS-level blocker:
|
Here are some examples of what cannot be blocked by a DNS-level blocker:
|
||||||
|
|
||||||
* YouTube, Twitch ads
|
* YouTube, Twitch ads;
|
||||||
* Facebook, Twitter, Instagram sponsored posts
|
|
||||||
|
|
||||||
Essentially, any advertising that shares a domain with content cannot be blocked by a DNS-level blocker.
|
* Facebook, Twitter, Instagram sponsored posts.
|
||||||
|
|
||||||
Is there a chance to handle this in the future? DNS will never be enough to do this. Our only option is to use a content blocking proxy like what we do in the standalone AdGuard applications. We're [going to bring](https://github.com/AdguardTeam/AdGuardHome/issues/1228) this feature support to AdGuard Home in the future. Unfortunately, even in this case, there still will be cases when this won't be enough or would require quite a complicated configuration.
|
Essentially, any advertising that shares a domain with content cannot be blocked
|
||||||
|
by a DNS-level blocker.
|
||||||
|
|
||||||
<a id="how-to-build"></a>
|
Is there a chance to handle this in the future? DNS will never be enough to do
|
||||||
## How to build from source
|
this. Our only option is to use a content blocking proxy like what we do in the
|
||||||
|
standalone AdGuard applications. We're [going to bring][issue-1228] this
|
||||||
|
feature support to AdGuard Home in the future. Unfortunately, even in this
|
||||||
|
case, there still will be cases when this won't be enough or would require quite
|
||||||
|
a complicated configuration.
|
||||||
|
|
||||||
### Prerequisites
|
[blog-adaway]: https://adguard.com/blog/adguard-vs-adaway-dns66.html
|
||||||
|
[issue-1228]: https://github.com/AdguardTeam/AdGuardHome/issues/1228
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a href="#how-to-build" id="how-to-build" name="how-to-build">How to build from source</a>
|
||||||
|
|
||||||
|
### <a href="#prerequisites" id="prerequisites" name="prerequisites">Prerequisites</a>
|
||||||
|
|
||||||
Run `make init` to prepare the development environment.
|
Run `make init` to prepare the development environment.
|
||||||
|
|
||||||
You will need this to build AdGuard Home:
|
You will need this to build AdGuard Home:
|
||||||
|
|
||||||
* [go](https://golang.org/dl/) v1.18 or later.
|
* [Go](https://golang.org/dl/) v1.18 or later;
|
||||||
* [node.js](https://nodejs.org/en/download/) v10.16.2 or later.
|
* [Node.js](https://nodejs.org/en/download/) v10.16.2 or later;
|
||||||
* [npm](https://www.npmjs.com/) v6.14 or later (temporary requirement, TODO: remove when redesign is finished).
|
* [npm](https://www.npmjs.com/) v6.14 or later;
|
||||||
* [yarn](https://yarnpkg.com/) v1.22.5 or later.
|
* [yarn](https://yarnpkg.com/) v1.22.5 or later.
|
||||||
|
|
||||||
### Building
|
|
||||||
|
|
||||||
Open Terminal and execute these commands:
|
|
||||||
|
### <a href="#building" id="building" name="building">Building</a>
|
||||||
|
|
||||||
|
Open your terminal and execute these commands:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://github.com/AdguardTeam/AdGuardHome
|
git clone https://github.com/AdguardTeam/AdGuardHome
|
||||||
|
@ -216,16 +266,18 @@ cd AdGuardHome
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
Please note, that the non-standard `-j` flag is currently not supported, so
|
**NOTE:** The non-standard `-j` flag is currently not supported, so building
|
||||||
building with `make -j 4` or setting your `MAKEFLAGS` to include, for example,
|
with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is
|
||||||
`-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to
|
likely to break the build. If you do have your `MAKEFLAGS` set to that, and you
|
||||||
that, and you don't want to change it, you can override it by running
|
don't want to change it, you can override it by running `make -j 1`.
|
||||||
`make -j 1`.
|
|
||||||
|
|
||||||
Check the [`Makefile`](https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile) to learn about other commands.
|
Check the [`Makefile`][src-makefile] to learn about other commands.
|
||||||
|
|
||||||
**Building for a different platform.** You can build AdGuard for any OS/ARCH just like any other Go project.
|
#### <a href="#building-cross" id="building-cross" name="building-cross">Building for a different platform</a>
|
||||||
In order to do this, specify `GOOS` and `GOARCH` env variables before running make.
|
|
||||||
|
You can build AdGuard Home for any OS/ARCH that Go supports. In order to do
|
||||||
|
this, specify `GOOS` and `GOARCH` environment variables as macros when running
|
||||||
|
`make`.
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -239,168 +291,223 @@ or:
|
||||||
make GOOS='linux' GOARCH='arm64'
|
make GOOS='linux' GOARCH='arm64'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Preparing release
|
#### <a href="#preparing-releases" id="preparing-releases" name="preparing-releases">Preparing releases</a>
|
||||||
|
|
||||||
You'll need this to prepare a release build:
|
You'll need [`snapcraft`] to prepare a release build. Once installed, run the
|
||||||
|
following command:
|
||||||
* [snapcraft](https://snapcraft.io/)
|
|
||||||
|
|
||||||
Commands:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
make build-release CHANNEL='...' VERSION='...'
|
make build-release CHANNEL='...' VERSION='...'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Docker image
|
See the [`build-release` target documentation][targ-release].
|
||||||
|
|
||||||
* Run `make build-docker` to build the Docker image locally (the one that we publish to DockerHub).
|
#### <a href="#docker-image" id="docker-image" name="docker-image">Docker image</a>
|
||||||
|
|
||||||
Please note, that we're using [Docker Buildx](https://docs.docker.com/buildx/working-with-buildx/) to build our official image.
|
Run `make build-docker` to build the Docker image locally (the one that we
|
||||||
|
publish to DockerHub). Please note, that we're using [Docker Buildx][buildx] to
|
||||||
|
build our official image.
|
||||||
|
|
||||||
You may need to prepare before using these builds:
|
You may need to prepare before using these builds:
|
||||||
|
|
||||||
* (Linux-only) Install Qemu: `docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes`
|
* (Linux-only) Install Qemu:
|
||||||
* Prepare builder: `docker buildx create --name buildx-builder --driver docker-container --use`
|
|
||||||
|
```sh
|
||||||
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes
|
||||||
|
```
|
||||||
|
|
||||||
|
* Prepare the builder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker buildx create --name buildx-builder --driver docker-container --use
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [`build-docker` target documentation][targ-docker].
|
||||||
|
|
||||||
|
#### <a href="#debugging-the-frontend" id="debugging-the-frontend" name="debugging-the-frontend">Debugging the frontend</a>
|
||||||
|
|
||||||
|
When you need to debug the frontend without recompiling the production version
|
||||||
|
every time, for example to check how your labels would look on a form, you can
|
||||||
|
run the frontend build a development environment.
|
||||||
|
|
||||||
|
1. In a separate terminal, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
( cd ./client/ && env NODE_ENV='development' npm run watch )
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run your `AdGuardHome` binary with the `--local-frontend` flag, which
|
||||||
|
instructs AdGuard Home to ignore the built-in frontend files and use those
|
||||||
|
from the `./build/` directory.
|
||||||
|
|
||||||
|
3. Now any changes you make in the `./client/` directory should be recompiled
|
||||||
|
and become available on the web UI. Make sure that you disable the browser
|
||||||
|
cache to make sure that you actually get the recompiled version.
|
||||||
|
|
||||||
|
[`snapcraft`]: https://snapcraft.io/
|
||||||
|
[buildx]: https://docs.docker.com/buildx/working-with-buildx/
|
||||||
|
[src-makefile]: https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile
|
||||||
|
[targ-docker]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-dockersh-build-a-multi-architecture-docker-image
|
||||||
|
[targ-release]: https://github.com/AdguardTeam/AdGuardHome/tree/master/scripts#build-releasesh-build-a-release-for-all-platforms
|
||||||
|
|
||||||
|
|
||||||
### Resources that we update periodically
|
|
||||||
|
|
||||||
* `scripts/translations`
|
## <a href="#contributing" id="contributing" name="contributing">Contributing</a>
|
||||||
* `scripts/whotracksme`
|
|
||||||
|
|
||||||
<a id="contributing"></a>
|
You are welcome to fork this repository, make your changes and [submit a pull
|
||||||
## Contributing
|
request][pr]. Please make sure you follow our [code guidelines][guide] though.
|
||||||
|
|
||||||
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
|
Please note that we don't expect people to contribute to both UI and backend
|
||||||
|
parts of the program simultaneously. Ideally, the backend part is implemented
|
||||||
|
first, i.e. configuration, API, and the functionality itself. The UI part can
|
||||||
|
be implemented later in a different pull request by a different person.
|
||||||
|
|
||||||
Please note that we don't expect people to contribute to both UI and golang parts of the program simultaneously. Ideally, the golang part is implemented first, i.e. configuration, API, and the functionality itself. The UI part can be implemented later in a different pull request by a different person.
|
[guide]: https://github.com/AdguardTeam/CodeGuidelines/
|
||||||
|
[pr]: https://github.com/AdguardTeam/AdGuardHome/pulls
|
||||||
|
|
||||||
<a id="test-unstable-versions"></a>
|
|
||||||
### Test unstable versions
|
|
||||||
|
### <a href="#test-unstable-versions" id="test-unstable-versions" name="test-unstable-versions">Test unstable versions</a>
|
||||||
|
|
||||||
There are two update channels that you can use:
|
There are two update channels that you can use:
|
||||||
|
|
||||||
* `beta` - beta version of AdGuard Home. More or less stable versions.
|
* `beta`: beta versions of AdGuard Home. More or less stable versions,
|
||||||
* `edge` - the newest version of AdGuard Home. New updates are pushed to this channel daily and it is the closest to the master branch you can get.
|
usually released every two weeks or more often.
|
||||||
|
|
||||||
|
* `edge`: the newest version of AdGuard Home from the development branch. New
|
||||||
|
updates are pushed to this channel daily.
|
||||||
|
|
||||||
There are three options how you can install an unstable version:
|
There are three options how you can install an unstable version:
|
||||||
|
|
||||||
1. [Snap Store](https://snapcraft.io/adguard-home) -- look for "beta" and "edge" channels there.
|
1. [Snap Store]: look for the `beta` and `edge` channels.
|
||||||
2. [Docker Hub](https://hub.docker.com/r/adguard/adguardhome) -- look for "beta" and "edge" tags there.
|
|
||||||
3. Standalone builds. Use the automated installation script or look for the available builds below.
|
|
||||||
|
|
||||||
Beta:
|
2. [Docker Hub]: look for the `beta` and `edge` tags.
|
||||||
|
|
||||||
```sh
|
3. Standalone builds. Use the automated installation script or look for the
|
||||||
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
|
available builds [on the Wiki][wiki-platf].
|
||||||
```
|
|
||||||
|
|
||||||
Edge:
|
Script to install a beta version:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
|
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
|
||||||
```
|
```
|
||||||
|
|
||||||
* Beta channel builds
|
Script to install an edge version:
|
||||||
* Linux: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_386.tar.gz)
|
|
||||||
* Linux ARM: [32-bit ARMv6](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi OS stable), [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_armv7.tar.gz)
|
|
||||||
* Linux MIPS: [32-bit MIPS](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adtidy.org/adguardhome/beta/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
|
||||||
* Windows: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_windows_386.zip)
|
|
||||||
* macOS: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_darwin_386.zip)
|
|
||||||
* macOS ARM: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_darwin_arm64.zip)
|
|
||||||
* FreeBSD: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_386.tar.gz)
|
|
||||||
* FreeBSD ARM: [64-bit](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adtidy.org/adguardhome/beta/AdGuardHome_freebsd_armv7.tar.gz)
|
|
||||||
* OpenBSD: (coming soon)
|
|
||||||
* OpenBSD ARM: (coming soon)
|
|
||||||
|
|
||||||
* Edge channel builds
|
```sh
|
||||||
* Linux: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_amd64.tar.gz), [32-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_386.tar.gz)
|
curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
|
||||||
* Linux ARM: [32-bit ARMv6](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_armv6.tar.gz) (recommended for Raspberry Pi OS stable), [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_arm64.tar.gz), [32-bit ARMv5](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_armv5.tar.gz), [32-bit ARMv7](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_armv7.tar.gz)
|
```
|
||||||
* Linux MIPS: [32-bit MIPS](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_mips_softfloat.tar.gz), [32-bit MIPSLE](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_mipsle_softfloat.tar.gz), [64-bit MIPS](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_mips64_softfloat.tar.gz), [64-bit MIPSLE](https://static.adtidy.org/adguardhome/edge/AdGuardHome_linux_mips64le_softfloat.tar.gz)
|
[wiki-platf]: https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms
|
||||||
* Windows: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_windows_amd64.zip), [32-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_windows_386.zip)
|
|
||||||
* macOS: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_darwin_amd64.zip), [32-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_darwin_386.zip)
|
|
||||||
* macOS ARM: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_darwin_arm64.zip)
|
|
||||||
* FreeBSD: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_amd64.tar.gz), [32-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_386.tar.gz)
|
|
||||||
* FreeBSD ARM: [64-bit](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_arm64.tar.gz), [32-bit ARMv5](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_armv5.tar.gz), [32-bit ARMv6](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_armv6.tar.gz), [32-bit ARMv7](https://static.adtidy.org/adguardhome/edge/AdGuardHome_freebsd_armv7.tar.gz)
|
|
||||||
* OpenBSD: [64-bit (experimental)](https://static.adtidy.org/adguardhome/edge/AdGuardHome_openbsd_amd64.tar.gz)
|
|
||||||
* OpenBSD ARM: [64-bit (experimental)](https://static.adtidy.org/adguardhome/edge/AdGuardHome_openbsd_arm64.tar.gz)
|
|
||||||
|
|
||||||
|
|
||||||
<a id="reporting-issues"></a>
|
|
||||||
### Report issues
|
|
||||||
|
|
||||||
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
|
### <a href="#reporting-issues" id="reporting-issues" name="reporting-issues">Report issues</a>
|
||||||
|
|
||||||
<a id="translate"></a>
|
If you run into any problem or have a suggestion, head to [this page][iss] and
|
||||||
### Help with translations
|
click on the “New issue” button.
|
||||||
|
|
||||||
|
[iss]: https://github.com/AdguardTeam/AdGuardHome/issues
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### <a href="#translate" id="translate" name="translate">Help with translations</a>
|
||||||
|
|
||||||
If you want to help with AdGuard Home translations, please learn more about
|
If you want to help with AdGuard Home translations, please learn more about
|
||||||
translating AdGuard products
|
translating AdGuard products [in our Knowledge Base][kb-trans]. You can
|
||||||
[in our Knowledge Base](https://kb.adguard.com/en/general/adguard-translations).
|
contribute to the [AdGuardHome project on CrowdIn][crowdin].
|
||||||
|
|
||||||
Here is a link to AdGuard Home project:
|
[crowdin]: https://crowdin.com/project/adguard-applications/en#/adguard-home
|
||||||
<https://crowdin.com/project/adguard-applications/en#/adguard-home>
|
[kb-trans]: https://kb.adguard.com/en/general/adguard-translations
|
||||||
|
|
||||||
<a id="help-other"></a>
|
|
||||||
### Other
|
|
||||||
|
|
||||||
Here's what you can also do to contribute:
|
|
||||||
|
|
||||||
1. [Look for issues][helpissues] marked as "help wanted".
|
### <a href="#help-other" id="help-other" name="help-other">Other</a>
|
||||||
2. Actualize the list of *Blocked services*. It can be found in
|
|
||||||
[filtering/blocked.go][blocked.go].
|
|
||||||
3. Actualize the list of known *trackers*. It it can be found in [this repo]
|
|
||||||
[companiesdb].
|
|
||||||
4. Actualize the list of vetted *blocklists*. It it can be found in
|
|
||||||
[client/src/helpers/filters/filters.json][filters.json].
|
|
||||||
|
|
||||||
[helpissues]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+
|
Another way you can contribute is by [looking for issues][iss-help] marked as
|
||||||
[blocked.go]: https://github.com/AdguardTeam/AdGuardHome/blob/master/internal/filtering/blocked.go
|
`help wanted`, asking if the issue is up for grabs, and sending a PR fixing the
|
||||||
[companiesdb]: https://github.com/AdguardTeam/companiesdb
|
bug or implementing the feature.
|
||||||
[filters.json]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/src/helpers/filters/filters.json
|
|
||||||
|
|
||||||
<a id="uses"></a>
|
[iss-help]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
|
||||||
## Projects that use AdGuard Home
|
|
||||||
|
|
||||||
* [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740) - iOS app by [Joost](https://rocketscience-it.nl/)
|
|
||||||
* [Python library](https://github.com/frenck/python-adguardhome) by [@frenck](https://github.com/frenck)
|
|
||||||
* [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home) by [@frenck](https://github.com/frenck)
|
|
||||||
* [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by [@kongfl888](https://github.com/kongfl888) (originally by [@rufengsuixing](https://github.com/rufengsuixing))
|
|
||||||
* [Prometheus exporter for AdGuard Home](https://github.com/ebrianne/adguard-exporter) by [@ebrianne](https://github.com/ebrianne)
|
|
||||||
* [AdGuard Home on GLInet routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by [Gl-Inet](https://gl-inet.com/)
|
|
||||||
* [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by [@gramakri](https://github.com/gramakri)
|
|
||||||
* [Asuswrt-Merlin-AdGuardHome-Installer](https://github.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer) by [@jumpsmm7](https://github.com/jumpsmm7) aka [@SomeWhereOverTheRainBow](https://www.snbforums.com/members/somewhereovertherainbow.64179/)
|
|
||||||
* [Node.js library](https://github.com/Andrea055/AdguardHomeAPI) by [@Andrea055](https://github.com/Andrea055/)
|
|
||||||
|
|
||||||
<a id="acknowledgments"></a>
|
|
||||||
## Acknowledgments
|
## <a href="#uses" id="uses" name="uses">Projects that use AdGuard Home</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO(a.garipov): Use reference links.
|
||||||
|
-->
|
||||||
|
|
||||||
|
* [AdGuard Home Remote](https://apps.apple.com/app/apple-store/id1543143740):
|
||||||
|
iOS app by [Joost](https://rocketscience-it.nl/).
|
||||||
|
|
||||||
|
* [Python library](https://github.com/frenck/python-adguardhome) by
|
||||||
|
[@frenck](https://github.com/frenck).
|
||||||
|
|
||||||
|
* [Home Assistant add-on](https://github.com/hassio-addons/addon-adguard-home)
|
||||||
|
by [@frenck](https://github.com/frenck).
|
||||||
|
|
||||||
|
* [OpenWrt LUCI app](https://github.com/kongfl888/luci-app-adguardhome) by
|
||||||
|
[@kongfl888](https://github.com/kongfl888) (originally by
|
||||||
|
[@rufengsuixing](https://github.com/rufengsuixing)).
|
||||||
|
|
||||||
|
* [Prometheus exporter for AdGuard
|
||||||
|
Home](https://github.com/ebrianne/adguard-exporter) by
|
||||||
|
[@ebrianne](https://github.com/ebrianne).
|
||||||
|
|
||||||
|
* [AdGuard Home on GLInet
|
||||||
|
routers](https://forum.gl-inet.com/t/adguardhome-on-gl-routers/10664) by
|
||||||
|
[Gl-Inet](https://gl-inet.com/).
|
||||||
|
|
||||||
|
* [Cloudron app](https://git.cloudron.io/cloudron/adguard-home-app) by
|
||||||
|
[@gramakri](https://github.com/gramakri).
|
||||||
|
|
||||||
|
* [Asuswrt-Merlin-AdGuardHome-Installer](https://github.com/jumpsmm7/Asuswrt-Merlin-AdGuardHome-Installer)
|
||||||
|
by [@jumpsmm7](https://github.com/jumpsmm7) aka
|
||||||
|
[@SomeWhereOverTheRainBow](https://www.snbforums.com/members/somewhereovertherainbow.64179/).
|
||||||
|
|
||||||
|
* [Node.js library](https://github.com/Andrea055/AdguardHomeAPI) by
|
||||||
|
[@Andrea055](https://github.com/Andrea055/).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a href="#acknowledgments" id="acknowledgments" name="acknowledgments">Acknowledgments</a>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO(a.garipov): Use reference links.
|
||||||
|
-->
|
||||||
|
|
||||||
This software wouldn't have been possible without:
|
This software wouldn't have been possible without:
|
||||||
|
|
||||||
* [Go](https://golang.org/dl/) and its libraries:
|
* [Go](https://golang.org/dl/) and its libraries:
|
||||||
* [gcache](https://github.com/bluele/gcache)
|
* [gcache](https://github.com/bluele/gcache)
|
||||||
* [miekg's dns](https://github.com/miekg/dns)
|
* [miekg's dns](https://github.com/miekg/dns)
|
||||||
* [go-yaml](https://github.com/go-yaml/yaml)
|
* [go-yaml](https://github.com/go-yaml/yaml)
|
||||||
* [service](https://godoc.org/github.com/kardianos/service)
|
* [service](https://godoc.org/github.com/kardianos/service)
|
||||||
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
|
* [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
|
||||||
* [urlfilter](https://github.com/AdguardTeam/urlfilter)
|
* [urlfilter](https://github.com/AdguardTeam/urlfilter)
|
||||||
* [Node.js](https://nodejs.org/) and its libraries:
|
* [Node.js](https://nodejs.org/) and its libraries:
|
||||||
* [React.js](https://reactjs.org)
|
* And many more Node.js packages.
|
||||||
* [Tabler](https://github.com/tabler/tabler)
|
* [React.js](https://reactjs.org)
|
||||||
* And many more node.js packages.
|
* [Tabler](https://github.com/tabler/tabler)
|
||||||
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
|
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
|
||||||
|
|
||||||
You might have seen that [CoreDNS](https://coredns.io) was mentioned here
|
You might have seen that [CoreDNS] was mentioned here before, but we've stopped
|
||||||
before, but we've stopped using it in AdGuard Home.
|
using it in AdGuard Home.
|
||||||
|
|
||||||
For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file.
|
For the full list of all Node.js packages in use, please take a look at
|
||||||
|
[`client/package.json`][src-packagejson] file.
|
||||||
|
|
||||||
<a id="privacy"></a>
|
[CoreDNS]: https://coredns.io
|
||||||
## Privacy
|
[src-packagejson]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## <a href="#privacy" id="privacy" name="privacy">Privacy</a>
|
||||||
|
|
||||||
Our main idea is that you are the one, who should be in control of your data.
|
Our main idea is that you are the one, who should be in control of your data.
|
||||||
So it is only natural, that AdGuard Home does not collect any usage statistics,
|
So it is only natural, that AdGuard Home does not collect any usage statistics,
|
||||||
and does not use any web services unless you configure it to do so. Full policy
|
and does not use any web services unless you configure it to do so. See also
|
||||||
with every bit that *could in theory be* sent by AdGuard Home is available
|
the [full privacy policy][privacy] with every bit that *could in theory be sent*
|
||||||
[here](https://adguard.com/en/privacy/home.html)
|
by AdGuard Home is available.
|
||||||
|
|
||||||
|
[privacy]: https://adguard.com/en/privacy/home.html
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
# Make sure to sync any changes with the branch overrides below.
|
# Make sure to sync any changes with the branch overrides below.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'edge'
|
'channel': 'edge'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Build frontend':
|
- 'Build frontend':
|
||||||
|
@ -322,7 +322,7 @@
|
||||||
# need to build a few of these.
|
# need to build a few of these.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'beta'
|
'channel': 'beta'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
||||||
# release-vX.Y.Z branches are the branches from which the actual final release
|
# release-vX.Y.Z branches are the branches from which the actual final release
|
||||||
# is built.
|
# is built.
|
||||||
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
- '^release-v[0-9]+\.[0-9]+\.[0-9]+':
|
||||||
|
@ -337,4 +337,4 @@
|
||||||
# are the ones that actually get released.
|
# are the ones that actually get released.
|
||||||
'variables':
|
'variables':
|
||||||
'channel': 'release'
|
'channel': 'release'
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
'key': 'AHBRTSPECS'
|
'key': 'AHBRTSPECS'
|
||||||
'name': 'AdGuard Home - Build and run tests'
|
'name': 'AdGuard Home - Build and run tests'
|
||||||
'variables':
|
'variables':
|
||||||
'dockerGo': 'adguard/golang-ubuntu:5.1'
|
'dockerGo': 'adguard/golang-ubuntu:5.2'
|
||||||
|
|
||||||
'stages':
|
'stages':
|
||||||
- 'Tests':
|
- 'Tests':
|
||||||
|
|
|
@ -215,6 +215,7 @@
|
||||||
"example_upstream_udp": "regular DNS (over UDP, hostname);",
|
"example_upstream_udp": "regular DNS (over UDP, hostname);",
|
||||||
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
|
"example_upstream_dot": "encrypted <0>DNS-over-TLS</0>;",
|
||||||
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>;",
|
"example_upstream_doh": "encrypted <0>DNS-over-HTTPS</0>;",
|
||||||
|
"example_upstream_doh3": "encrypted DNS-over-HTTPS with forced <0>HTTP/3</0> and no fallback to HTTP/2 or below;",
|
||||||
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
|
"example_upstream_doq": "encrypted <0>DNS-over-QUIC</0>;",
|
||||||
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
|
"example_upstream_sdns": "<0>DNS Stamps</0> for <1>DNSCrypt</1> or <2>DNS-over-HTTPS</2> resolvers;",
|
||||||
"example_upstream_tcp": "regular DNS (over TCP);",
|
"example_upstream_tcp": "regular DNS (over TCP);",
|
||||||
|
@ -605,7 +606,7 @@
|
||||||
"blocklist": "Blocklist",
|
"blocklist": "Blocklist",
|
||||||
"milliseconds_abbreviation": "ms",
|
"milliseconds_abbreviation": "ms",
|
||||||
"cache_size": "Cache size",
|
"cache_size": "Cache size",
|
||||||
"cache_size_desc": "DNS cache size (in bytes).",
|
"cache_size_desc": "DNS cache size (in bytes). To disable caching, leave empty.",
|
||||||
"cache_ttl_min_override": "Override minimum TTL",
|
"cache_ttl_min_override": "Override minimum TTL",
|
||||||
"cache_ttl_max_override": "Override maximum TTL",
|
"cache_ttl_max_override": "Override maximum TTL",
|
||||||
"enter_cache_size": "Enter cache size (bytes)",
|
"enter_cache_size": "Enter cache size (bytes)",
|
||||||
|
|
|
@ -121,7 +121,7 @@ const ClientCell = ({
|
||||||
{options.map(({ name, onClick, disabled }) => (
|
{options.map(({ name, onClick, disabled }) => (
|
||||||
<button
|
<button
|
||||||
key={name}
|
key={name}
|
||||||
className="button-action--arrow-option px-4 py-2"
|
className="button-action--arrow-option px-4 py-1"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
|
|
|
@ -50,9 +50,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.grid .key-colon, .grid .title--border {
|
.grid .title--border {
|
||||||
|
margin-bottom: 4px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid .key-colon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--gray-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid__row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 14px;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid__row .filteringRules__filter,
|
||||||
|
.grid__row .filteringRules {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767.98px) {
|
@media (max-width: 767.98px) {
|
||||||
|
@ -100,7 +121,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.title--border {
|
.title--border {
|
||||||
padding-top: 2rem;
|
padding-top: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title--border:before {
|
.title--border:before {
|
||||||
|
@ -109,7 +130,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
border-top: 0.5px solid var(--gray-d8) !important;
|
border-top: 0.5px solid var(--gray-d8) !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: -1rem;
|
margin-top: -0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-cross {
|
.icon-cross {
|
||||||
|
|
|
@ -146,7 +146,7 @@ const Row = memo(({
|
||||||
type="button"
|
type="button"
|
||||||
className={
|
className={
|
||||||
classNames(
|
classNames(
|
||||||
'button-action--arrow-option',
|
'button-action--arrow-option mb-1',
|
||||||
{ 'bg--danger': !isBlocked },
|
{ 'bg--danger': !isBlocked },
|
||||||
{ 'bg--green': isFiltered },
|
{ 'bg--green': isFiltered },
|
||||||
)}
|
)}
|
||||||
|
@ -158,13 +158,13 @@ const Row = memo(({
|
||||||
);
|
);
|
||||||
|
|
||||||
const blockForClientButton = <button
|
const blockForClientButton = <button
|
||||||
className='text-center font-weight-bold py-2 button-action--arrow-option'
|
className='text-center font-weight-bold py-1 button-action--arrow-option'
|
||||||
onClick={onBlockingForClientClick}>
|
onClick={onBlockingForClientClick}>
|
||||||
{t(blockingForClientKey)}
|
{t(blockingForClientKey)}
|
||||||
</button>;
|
</button>;
|
||||||
|
|
||||||
const blockClientButton = <button
|
const blockClientButton = <button
|
||||||
className='text-center font-weight-bold py-2 button-action--arrow-option'
|
className='text-center font-weight-bold py-1 button-action--arrow-option'
|
||||||
onClick={onBlockingClientClick}
|
onClick={onBlockingClientClick}
|
||||||
disabled={processingSet || lastRuleInAllowlist}>
|
disabled={processingSet || lastRuleInAllowlist}>
|
||||||
{t(blockingClientKey)}
|
{t(blockingClientKey)}
|
||||||
|
|
|
@ -312,8 +312,8 @@
|
||||||
border: 0;
|
border: 0;
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.2rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
|
|
@ -47,17 +47,20 @@ const processContent = (data) => Object.entries(data)
|
||||||
keyClass = '';
|
keyClass = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return isHidden ? null : <div key={key}>
|
return isHidden ? null : (
|
||||||
<div
|
<div className="grid__row" key={key}>
|
||||||
|
<div
|
||||||
className={classNames(`key__${key}`, keyClass, {
|
className={classNames(`key__${key}`, keyClass, {
|
||||||
'font-weight-bold': isBoolean && value === true,
|
'font-weight-bold': isBoolean && value === true,
|
||||||
})}>
|
})}
|
||||||
<Trans>{isButton ? value : key}</Trans>
|
>
|
||||||
|
<Trans>{isButton ? value : key}</Trans>
|
||||||
|
</div>
|
||||||
|
<div className={`value__${key} text-pre text-truncate`}>
|
||||||
|
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`value__${key} text-pre text-truncate`}>
|
);
|
||||||
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
|
|
||||||
</div>
|
|
||||||
</div>;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const Logs = () => {
|
const Logs = () => {
|
||||||
|
|
|
@ -57,6 +57,22 @@ const Examples = (props) => (
|
||||||
example_upstream_doh
|
example_upstream_doh
|
||||||
</Trans>
|
</Trans>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>h3://unfiltered.adguard-dns.com/dns-query</code>: <Trans
|
||||||
|
components={[
|
||||||
|
<a
|
||||||
|
href="https://en.wikipedia.org/wiki/HTTP/3"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
key="0"
|
||||||
|
>
|
||||||
|
HTTP/3
|
||||||
|
</a>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
example_upstream_doh3
|
||||||
|
</Trans>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
|
<code>quic://unfiltered.adguard-dns.com</code>: <Trans
|
||||||
components={[
|
components={[
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -3,7 +3,7 @@ module github.com/AdguardTeam/AdGuardHome
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2
|
github.com/AdguardTeam/dnsproxy v0.45.3
|
||||||
github.com/AdguardTeam/golibs v0.10.9
|
github.com/AdguardTeam/golibs v0.10.9
|
||||||
github.com/AdguardTeam/urlfilter v0.16.0
|
github.com/AdguardTeam/urlfilter v0.16.0
|
||||||
github.com/NYTimes/gziphandler v1.1.1
|
github.com/NYTimes/gziphandler v1.1.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,5 +1,5 @@
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2 h1:K9BXkQAfAKjrzbWbczpA2IA1owLe/edv0nG0e2+Esko=
|
github.com/AdguardTeam/dnsproxy v0.45.3 h1:lvJlifDIVjHFVkVcieBhXyQA357Wl+vmLxeDlaQ8DE8=
|
||||||
github.com/AdguardTeam/dnsproxy v0.45.2/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
|
github.com/AdguardTeam/dnsproxy v0.45.3/go.mod h1:h+0r4GDvHHY2Wu6r7oqva+O37h00KofYysfzy1TEXFE=
|
||||||
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
github.com/AdguardTeam/golibs v0.4.0/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
|
||||||
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
github.com/AdguardTeam/golibs v0.10.4/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
|
||||||
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
github.com/AdguardTeam/golibs v0.10.9 h1:F9oP2da0dQ9RQDM1lGR7LxUTfUWu8hEFOs4icwAkKM0=
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Package aghchan contains channel utilities.
|
||||||
|
package aghchan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Receive returns an error if it cannot receive a value form c before timeout
|
||||||
|
// runs out.
|
||||||
|
func Receive[T any](c <-chan T, timeout time.Duration) (v T, ok bool, err error) {
|
||||||
|
var zero T
|
||||||
|
timeoutCh := time.After(timeout)
|
||||||
|
select {
|
||||||
|
case <-timeoutCh:
|
||||||
|
// TODO(a.garipov): Consider implementing [errors.Aser] for
|
||||||
|
// os.ErrTimeout.
|
||||||
|
return zero, false, fmt.Errorf("did not receive after %s", timeout)
|
||||||
|
case v, ok = <-c:
|
||||||
|
return v, ok, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustReceive panics if it cannot receive a value form c before timeout runs
|
||||||
|
// out.
|
||||||
|
func MustReceive[T any](c <-chan T, timeout time.Duration) (v T, ok bool) {
|
||||||
|
v, ok, err := Receive(c, timeout)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return v, ok
|
||||||
|
}
|
|
@ -62,9 +62,16 @@ func WriteTextPlainDeprecated(w http.ResponseWriter, r *http.Request) (isPlainTe
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteJSONResponse sets the content-type header in w.Header() to
|
// WriteJSONResponse sets the content-type header in w.Header() to
|
||||||
// "application/json", encodes resp to w, calls Error on any returned error, and
|
// "application/json", writes a header with a "200 OK" status, encodes resp to
|
||||||
// returns it as well.
|
// w, calls [Error] on any returned error, and returns it as well.
|
||||||
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
|
func WriteJSONResponse(w http.ResponseWriter, r *http.Request, resp any) (err error) {
|
||||||
|
return WriteJSONResponseCode(w, r, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteJSONResponseCode is like [WriteJSONResponse] but adds the ability to
|
||||||
|
// redefine the status code.
|
||||||
|
func WriteJSONResponseCode(w http.ResponseWriter, r *http.Request, code int, resp any) (err error) {
|
||||||
|
w.WriteHeader(code)
|
||||||
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
|
w.Header().Set(HdrNameContentType, HdrValApplicationJSON)
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
err = json.NewEncoder(w).Encode(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,11 +8,14 @@ package aghhttp
|
||||||
const (
|
const (
|
||||||
HdrNameAcceptEncoding = "Accept-Encoding"
|
HdrNameAcceptEncoding = "Accept-Encoding"
|
||||||
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
HdrNameAccessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||||
|
HdrNameAltSvc = "Alt-Svc"
|
||||||
HdrNameContentEncoding = "Content-Encoding"
|
HdrNameContentEncoding = "Content-Encoding"
|
||||||
HdrNameContentType = "Content-Type"
|
HdrNameContentType = "Content-Type"
|
||||||
|
HdrNameOrigin = "Origin"
|
||||||
HdrNameServer = "Server"
|
HdrNameServer = "Server"
|
||||||
HdrNameTrailer = "Trailer"
|
HdrNameTrailer = "Trailer"
|
||||||
HdrNameUserAgent = "User-Agent"
|
HdrNameUserAgent = "User-Agent"
|
||||||
|
HdrNameVary = "Vary"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP header value constants.
|
// HTTP header value constants.
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -38,48 +39,44 @@ func checkOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ifaceIPv4Subnet returns the first suitable IPv4 subnetwork iface has.
|
// ifaceIPv4Subnet returns the first suitable IPv4 subnetwork iface has.
|
||||||
func ifaceIPv4Subnet(iface *net.Interface) (subnet *net.IPNet, err error) {
|
func ifaceIPv4Subnet(iface *net.Interface) (subnet netip.Prefix, err error) {
|
||||||
var addrs []net.Addr
|
var addrs []net.Addr
|
||||||
if addrs, err = iface.Addrs(); err != nil {
|
if addrs, err = iface.Addrs(); err != nil {
|
||||||
return nil, err
|
return netip.Prefix{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, a := range addrs {
|
for _, a := range addrs {
|
||||||
|
var ip net.IP
|
||||||
|
var maskLen int
|
||||||
switch a := a.(type) {
|
switch a := a.(type) {
|
||||||
case *net.IPAddr:
|
case *net.IPAddr:
|
||||||
subnet = &net.IPNet{
|
ip = a.IP
|
||||||
IP: a.IP,
|
maskLen, _ = ip.DefaultMask().Size()
|
||||||
Mask: a.IP.DefaultMask(),
|
|
||||||
}
|
|
||||||
case *net.IPNet:
|
case *net.IPNet:
|
||||||
subnet = a
|
ip = a.IP
|
||||||
|
maskLen, _ = a.Mask.Size()
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip4 := subnet.IP.To4(); ip4 != nil {
|
if ip = ip.To4(); ip != nil {
|
||||||
subnet.IP = ip4
|
return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ip)), maskLen), nil
|
||||||
|
|
||||||
return subnet, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("interface %s has no ipv4 addresses", iface.Name)
|
return netip.Prefix{}, fmt.Errorf("interface %s has no ipv4 addresses", iface.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkOtherDHCPv4 sends a DHCP request to the specified network interface, and
|
// checkOtherDHCPv4 sends a DHCP request to the specified network interface, and
|
||||||
// waits for a response for a period defined by defaultDiscoverTime.
|
// waits for a response for a period defined by defaultDiscoverTime.
|
||||||
func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) {
|
func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) {
|
||||||
var subnet *net.IPNet
|
var subnet netip.Prefix
|
||||||
if subnet, err = ifaceIPv4Subnet(iface); err != nil {
|
if subnet, err = ifaceIPv4Subnet(iface); err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve broadcast addr.
|
// Resolve broadcast addr.
|
||||||
dst := netutil.IPPort{
|
dst := netip.AddrPortFrom(BroadcastFromPref(subnet), 67).String()
|
||||||
IP: BroadcastFromIPNet(subnet),
|
|
||||||
Port: 67,
|
|
||||||
}.String()
|
|
||||||
var dstAddr *net.UDPAddr
|
var dstAddr *net.UDPAddr
|
||||||
if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil {
|
if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil {
|
||||||
return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err)
|
return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err)
|
||||||
|
|
|
@ -106,9 +106,13 @@ type HostsContainer struct {
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
|
|
||||||
// updates is the channel for receiving updated hosts.
|
// updates is the channel for receiving updated hosts.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
|
||||||
updates chan *netutil.IPMap
|
updates chan *netutil.IPMap
|
||||||
|
|
||||||
// last is the set of hosts that was cached within last detected change.
|
// last is the set of hosts that was cached within last detected change.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
|
||||||
last *netutil.IPMap
|
last *netutil.IPMap
|
||||||
|
|
||||||
// fsys is the working file system to read hosts files from.
|
// fsys is the working file system to read hosts files from.
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
"github.com/AdguardTeam/golibs/stringutil"
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/AdguardTeam/urlfilter"
|
"github.com/AdguardTeam/urlfilter"
|
||||||
|
@ -163,15 +163,9 @@ func TestHostsContainer_refresh(t *testing.T) {
|
||||||
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
checkRefresh := func(t *testing.T, want *HostsRecord) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
var ok bool
|
upd, ok := aghchan.MustReceive(hc.Upd(), 1*time.Second)
|
||||||
var upd *netutil.IPMap
|
require.True(t, ok)
|
||||||
select {
|
require.NotNil(t, upd)
|
||||||
case upd, ok = <-hc.Upd():
|
|
||||||
require.True(t, ok)
|
|
||||||
require.NotNil(t, upd)
|
|
||||||
case <-time.After(1 * time.Second):
|
|
||||||
t.Fatal("did not receive after 1s")
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, 1, upd.Len())
|
assert.Equal(t, 1, upd.Len())
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Variables and functions to substitute in tests.
|
// Variables and functions to substitute in tests.
|
||||||
|
@ -31,6 +31,12 @@ var (
|
||||||
// the IP being static is available.
|
// the IP being static is available.
|
||||||
const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
const ErrNoStaticIPInfo errors.Error = "no information about static ip"
|
||||||
|
|
||||||
|
// IPv4Localhost returns 127.0.0.1, which returns true for [netip.Addr.Is4].
|
||||||
|
func IPv4Localhost() (ip netip.Addr) { return netip.AddrFrom4([4]byte{127, 0, 0, 1}) }
|
||||||
|
|
||||||
|
// IPv6Localhost returns ::1, which returns true for [netip.Addr.Is6].
|
||||||
|
func IPv6Localhost() (ip netip.Addr) { return netip.AddrFrom16([16]byte{15: 1}) }
|
||||||
|
|
||||||
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
// IfaceHasStaticIP checks if interface is configured to have static IP address.
|
||||||
// If it can't give a definitive answer, it returns false and an error for which
|
// If it can't give a definitive answer, it returns false and an error for which
|
||||||
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
// errors.Is(err, ErrNoStaticIPInfo) is true.
|
||||||
|
@ -47,26 +53,31 @@ func IfaceSetStaticIP(ifaceName string) (err error) {
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
// TODO(e.burkov): Investigate if the gateway address may be fetched in another
|
||||||
// way since not every machine has the software installed.
|
// way since not every machine has the software installed.
|
||||||
func GatewayIP(ifaceName string) (ip net.IP) {
|
func GatewayIP(ifaceName string) (ip netip.Addr) {
|
||||||
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
code, out, err := aghosRunCommand("ip", "route", "show", "dev", ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug("%s", err)
|
log.Debug("%s", err)
|
||||||
|
|
||||||
return nil
|
return netip.Addr{}
|
||||||
} else if code != 0 {
|
} else if code != 0 {
|
||||||
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
|
||||||
|
|
||||||
return nil
|
return netip.Addr{}
|
||||||
}
|
}
|
||||||
|
|
||||||
fields := bytes.Fields(out)
|
fields := bytes.Fields(out)
|
||||||
// The meaningful "ip route" command output should contain the word
|
// The meaningful "ip route" command output should contain the word
|
||||||
// "default" at first field and default gateway IP address at third field.
|
// "default" at first field and default gateway IP address at third field.
|
||||||
if len(fields) < 3 || string(fields[0]) != "default" {
|
if len(fields) < 3 || string(fields[0]) != "default" {
|
||||||
return nil
|
return netip.Addr{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return net.ParseIP(string(fields[2]))
|
ip, err = netip.ParseAddr(string(fields[2]))
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip
|
||||||
}
|
}
|
||||||
|
|
||||||
// CanBindPrivilegedPorts checks if current process can bind to privileged
|
// CanBindPrivilegedPorts checks if current process can bind to privileged
|
||||||
|
@ -78,9 +89,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
|
||||||
// NetInterface represents an entry of network interfaces map.
|
// NetInterface represents an entry of network interfaces map.
|
||||||
type NetInterface struct {
|
type NetInterface struct {
|
||||||
// Addresses are the network interface addresses.
|
// Addresses are the network interface addresses.
|
||||||
Addresses []net.IP `json:"ip_addresses,omitempty"`
|
Addresses []netip.Addr `json:"ip_addresses,omitempty"`
|
||||||
// Subnets are the IP networks for this network interface.
|
// Subnets are the IP networks for this network interface.
|
||||||
Subnets []*net.IPNet `json:"-"`
|
Subnets []netip.Prefix `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HardwareAddr net.HardwareAddr `json:"hardware_address"`
|
HardwareAddr net.HardwareAddr `json:"hardware_address"`
|
||||||
Flags net.Flags `json:"flags"`
|
Flags net.Flags `json:"flags"`
|
||||||
|
@ -101,57 +112,79 @@ func (iface NetInterface) MarshalJSON() ([]byte, error) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NetInterfaceFrom(iface *net.Interface) (niface *NetInterface, err error) {
|
||||||
|
niface = &NetInterface{
|
||||||
|
Name: iface.Name,
|
||||||
|
HardwareAddr: iface.HardwareAddr,
|
||||||
|
Flags: iface.Flags,
|
||||||
|
MTU: iface.MTU,
|
||||||
|
}
|
||||||
|
|
||||||
|
addrs, err := iface.Addrs()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect network interface addresses.
|
||||||
|
for _, addr := range addrs {
|
||||||
|
n, ok := addr.(*net.IPNet)
|
||||||
|
if !ok {
|
||||||
|
// Should be *net.IPNet, this is weird.
|
||||||
|
return nil, fmt.Errorf("expected %[2]s to be %[1]T, got %[2]T", n, addr)
|
||||||
|
} else if ip4 := n.IP.To4(); ip4 != nil {
|
||||||
|
n.IP = ip4
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, ok := netip.AddrFromSlice(n.IP)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("bad address %s", n.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.Unmap()
|
||||||
|
if ip.IsLinkLocalUnicast() {
|
||||||
|
// Ignore link-local IPv4.
|
||||||
|
if ip.Is4() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ip = ip.WithZone(iface.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
ones, _ := n.Mask.Size()
|
||||||
|
p := netip.PrefixFrom(ip, ones)
|
||||||
|
|
||||||
|
niface.Addresses = append(niface.Addresses, ip)
|
||||||
|
niface.Subnets = append(niface.Subnets, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
return niface, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
|
// GetValidNetInterfacesForWeb returns interfaces that are eligible for DNS and
|
||||||
// WEB only we do not return link-local addresses here.
|
// WEB only we do not return link-local addresses here.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Can't properly test the function since it's nontrivial to
|
// TODO(e.burkov): Can't properly test the function since it's nontrivial to
|
||||||
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
|
// substitute net.Interface.Addrs and the net.InterfaceAddrs can't be used.
|
||||||
func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
|
||||||
ifaces, err := net.Interfaces()
|
ifaces, err := net.Interfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't get interfaces: %w", err)
|
return nil, fmt.Errorf("getting interfaces: %w", err)
|
||||||
} else if len(ifaces) == 0 {
|
} else if len(ifaces) == 0 {
|
||||||
return nil, errors.Error("couldn't find any legible interface")
|
return nil, errors.Error("no legible interfaces")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
for i := range ifaces {
|
||||||
var addrs []net.Addr
|
var niface *NetInterface
|
||||||
addrs, err = iface.Addrs()
|
niface, err = NetInterfaceFrom(&ifaces[i])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
|
return nil, err
|
||||||
}
|
} else if len(niface.Addresses) != 0 {
|
||||||
|
// Discard interfaces with no addresses.
|
||||||
netIface := &NetInterface{
|
nifaces = append(nifaces, niface)
|
||||||
MTU: iface.MTU,
|
|
||||||
Name: iface.Name,
|
|
||||||
HardwareAddr: iface.HardwareAddr,
|
|
||||||
Flags: iface.Flags,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect network interface addresses.
|
|
||||||
for _, addr := range addrs {
|
|
||||||
ipNet, ok := addr.(*net.IPNet)
|
|
||||||
if !ok {
|
|
||||||
// Should be net.IPNet, this is weird.
|
|
||||||
return nil, fmt.Errorf("got %s that is not net.IPNet, it is %T", addr, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore link-local.
|
|
||||||
if ipNet.IP.IsLinkLocalUnicast() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
netIface.Addresses = append(netIface.Addresses, ipNet.IP)
|
|
||||||
netIface.Subnets = append(netIface.Subnets, ipNet)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard interfaces with no addresses.
|
|
||||||
if len(netIface.Addresses) != 0 {
|
|
||||||
netIfaces = append(netIfaces, netIface)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return netIfaces, nil
|
return nifaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InterfaceByIP returns the name of the interface bound to ip.
|
// InterfaceByIP returns the name of the interface bound to ip.
|
||||||
|
@ -160,7 +193,7 @@ func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
|
||||||
// IP address can be shared by multiple interfaces in some configurations.
|
// IP address can be shared by multiple interfaces in some configurations.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||||
func InterfaceByIP(ip net.IP) (ifaceName string) {
|
func InterfaceByIP(ip netip.Addr) (ifaceName string) {
|
||||||
ifaces, err := GetValidNetInterfacesForWeb()
|
ifaces, err := GetValidNetInterfacesForWeb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
|
@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
||||||
|
|
||||||
for _, iface := range ifaces {
|
for _, iface := range ifaces {
|
||||||
for _, addr := range iface.Addresses {
|
for _, addr := range iface.Addresses {
|
||||||
if ip.Equal(addr) {
|
if ip == addr {
|
||||||
return iface.Name
|
return iface.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSubnet returns pointer to net.IPNet for the specified interface or nil if
|
// GetSubnet returns the subnet corresponding to the interface of zero prefix if
|
||||||
// the search fails.
|
// the search fails.
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
|
||||||
func GetSubnet(ifaceName string) *net.IPNet {
|
func GetSubnet(ifaceName string) (p netip.Prefix) {
|
||||||
netIfaces, err := GetValidNetInterfacesForWeb()
|
netIfaces, err := GetValidNetInterfacesForWeb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Could not get network interfaces info: %v", err)
|
log.Error("Could not get network interfaces info: %v", err)
|
||||||
return nil
|
|
||||||
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, netIface := range netIfaces {
|
for _, netIface := range netIfaces {
|
||||||
|
@ -194,14 +228,14 @@ func GetSubnet(ifaceName string) *net.IPNet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPort checks if the port is available for binding. network is expected
|
// CheckPort checks if the port is available for binding. network is expected
|
||||||
// to be one of "udp" and "tcp".
|
// to be one of "udp" and "tcp".
|
||||||
func CheckPort(network string, ip net.IP, port int) (err error) {
|
func CheckPort(network string, ipp netip.AddrPort) (err error) {
|
||||||
var c io.Closer
|
var c io.Closer
|
||||||
addr := netutil.IPPort{IP: ip, Port: port}.String()
|
addr := ipp.String()
|
||||||
switch network {
|
switch network {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
c, err = net.Listen(network, addr)
|
c, err = net.Listen(network, addr)
|
||||||
|
@ -251,18 +285,23 @@ func CollectAllIfacesAddrs() (addrs []string, err error) {
|
||||||
return addrs, nil
|
return addrs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BroadcastFromIPNet calculates the broadcast IP address for n.
|
// BroadcastFromPref calculates the broadcast IP address for p.
|
||||||
func BroadcastFromIPNet(n *net.IPNet) (dc net.IP) {
|
func BroadcastFromPref(p netip.Prefix) (bc netip.Addr) {
|
||||||
dc = netutil.CloneIP(n.IP)
|
bc = p.Addr().Unmap()
|
||||||
|
if !bc.IsValid() {
|
||||||
mask := n.Mask
|
return netip.Addr{}
|
||||||
if mask == nil {
|
|
||||||
mask = dc.DefaultMask()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, b := range mask {
|
maskLen, addrLen := p.Bits(), bc.BitLen()
|
||||||
dc[i] |= ^b
|
if maskLen == addrLen {
|
||||||
|
return bc
|
||||||
}
|
}
|
||||||
|
|
||||||
return dc
|
ipBytes := bc.AsSlice()
|
||||||
|
for i := maskLen; i < addrLen; i++ {
|
||||||
|
ipBytes[i/8] |= 1 << (7 - (i % 8))
|
||||||
|
}
|
||||||
|
bc, _ = netip.AddrFromSlice(ipBytes)
|
||||||
|
|
||||||
|
return bc
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
|
||||||
// interface through dhcpcd.conf.
|
// interface through dhcpcd.conf.
|
||||||
func ifaceSetStaticIP(ifaceName string) (err error) {
|
func ifaceSetStaticIP(ifaceName string) (err error) {
|
||||||
ipNet := GetSubnet(ifaceName)
|
ipNet := GetSubnet(ifaceName)
|
||||||
if ipNet.IP == nil {
|
if !ipNet.Addr().IsValid() {
|
||||||
return errors.Error("can't get IP address")
|
return errors.Error("can't get IP address")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ func ifaceSetStaticIP(ifaceName string) (err error) {
|
||||||
|
|
||||||
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
|
// dhcpcdConfIface returns configuration lines for the dhcpdc.conf files that
|
||||||
// configure the interface to have a static IP.
|
// configure the interface to have a static IP.
|
||||||
func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf string) {
|
func dhcpcdConfIface(ifaceName string, subnet netip.Prefix, gateway netip.Addr) (conf string) {
|
||||||
b := &strings.Builder{}
|
b := &strings.Builder{}
|
||||||
stringutil.WriteToBuilder(
|
stringutil.WriteToBuilder(
|
||||||
b,
|
b,
|
||||||
|
@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
|
||||||
" added by AdGuard Home.\ninterface ",
|
" added by AdGuard Home.\ninterface ",
|
||||||
ifaceName,
|
ifaceName,
|
||||||
"\nstatic ip_address=",
|
"\nstatic ip_address=",
|
||||||
ipNet.String(),
|
subnet.String(),
|
||||||
"\n",
|
"\n",
|
||||||
)
|
)
|
||||||
|
|
||||||
if gwIP != nil {
|
if gateway != (netip.Addr{}) {
|
||||||
stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n")
|
stringutil.WriteToBuilder(b, "static routers=", gateway.String(), "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
stringutil.WriteToBuilder(b, "static domain_name_servers=", ipNet.IP.String(), "\n\n")
|
stringutil.WriteToBuilder(b, "static domain_name_servers=", subnet.Addr().String(), "\n\n")
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -93,34 +94,29 @@ func TestGatewayIP(t *testing.T) {
|
||||||
const cmd = "ip route show dev " + ifaceName
|
const cmd = "ip route show dev " + ifaceName
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
|
||||||
shell mapShell
|
shell mapShell
|
||||||
want net.IP
|
want netip.Addr
|
||||||
|
name string
|
||||||
}{{
|
}{{
|
||||||
name: "success_v4",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `default via 1.2.3.4 onlink`, nil),
|
||||||
want: net.IP{1, 2, 3, 4}.To16(),
|
want: netip.MustParseAddr("1.2.3.4"),
|
||||||
|
name: "success_v4",
|
||||||
}, {
|
}, {
|
||||||
name: "success_v6",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `default via ::ffff onlink`, nil),
|
||||||
want: net.IP{
|
want: netip.MustParseAddr("::ffff"),
|
||||||
0x0, 0x0, 0x0, 0x0,
|
name: "success_v6",
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0x0, 0x0,
|
|
||||||
0x0, 0x0, 0xFF, 0xFF,
|
|
||||||
},
|
|
||||||
}, {
|
}, {
|
||||||
name: "bad_output",
|
|
||||||
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
shell: theOnlyCmd(cmd, 0, `non-default via 1.2.3.4 onlink`, nil),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "bad_output",
|
||||||
}, {
|
}, {
|
||||||
name: "err_runcmd",
|
|
||||||
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
shell: theOnlyCmd(cmd, 0, "", errors.Error("can't run command")),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "err_runcmd",
|
||||||
}, {
|
}, {
|
||||||
name: "bad_code",
|
|
||||||
shell: theOnlyCmd(cmd, 1, "", nil),
|
shell: theOnlyCmd(cmd, 1, "", nil),
|
||||||
want: nil,
|
want: netip.Addr{},
|
||||||
|
name: "bad_code",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -150,65 +146,64 @@ func TestInterfaceByIP(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBroadcastFromIPNet(t *testing.T) {
|
func TestBroadcastFromIPNet(t *testing.T) {
|
||||||
known6 := net.IP{
|
known4 := netip.MustParseAddr("192.168.0.1")
|
||||||
1, 2, 3, 4,
|
fullBroadcast4 := netip.MustParseAddr("255.255.255.255")
|
||||||
5, 6, 7, 8,
|
|
||||||
9, 10, 11, 12,
|
known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10")
|
||||||
13, 14, 15, 16,
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
pref netip.Prefix
|
||||||
subnet *net.IPNet
|
want netip.Addr
|
||||||
want net.IP
|
name string
|
||||||
}{{
|
}{{
|
||||||
|
pref: netip.PrefixFrom(known4, 0),
|
||||||
|
want: fullBroadcast4,
|
||||||
name: "full",
|
name: "full",
|
||||||
subnet: &net.IPNet{
|
|
||||||
IP: net.IP{192, 168, 0, 1},
|
|
||||||
Mask: net.IPMask{255, 255, 15, 0},
|
|
||||||
},
|
|
||||||
want: net.IP{192, 168, 240, 255},
|
|
||||||
}, {
|
}, {
|
||||||
name: "ipv6_no_mask",
|
pref: netip.PrefixFrom(known4, 20),
|
||||||
subnet: &net.IPNet{
|
want: netip.MustParseAddr("192.168.15.255"),
|
||||||
IP: known6,
|
name: "full",
|
||||||
},
|
}, {
|
||||||
|
pref: netip.PrefixFrom(known6, netutil.IPv6BitLen),
|
||||||
want: known6,
|
want: known6,
|
||||||
|
name: "ipv6_no_mask",
|
||||||
}, {
|
}, {
|
||||||
|
pref: netip.PrefixFrom(known4, netutil.IPv4BitLen),
|
||||||
|
want: known4,
|
||||||
name: "ipv4_no_mask",
|
name: "ipv4_no_mask",
|
||||||
subnet: &net.IPNet{
|
|
||||||
IP: net.IP{192, 168, 1, 2},
|
|
||||||
},
|
|
||||||
want: net.IP{192, 168, 1, 255},
|
|
||||||
}, {
|
}, {
|
||||||
|
pref: netip.PrefixFrom(netip.IPv4Unspecified(), 0),
|
||||||
|
want: fullBroadcast4,
|
||||||
name: "unspecified",
|
name: "unspecified",
|
||||||
subnet: &net.IPNet{
|
}, {
|
||||||
IP: net.IP{0, 0, 0, 0},
|
pref: netip.Prefix{},
|
||||||
Mask: net.IPMask{0, 0, 0, 0},
|
want: netip.Addr{},
|
||||||
},
|
name: "invalid",
|
||||||
want: net.IPv4bcast,
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
bc := BroadcastFromIPNet(tc.subnet)
|
assert.Equal(t, tc.want, BroadcastFromPref(tc.pref))
|
||||||
assert.True(t, bc.Equal(tc.want), bc)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckPort(t *testing.T) {
|
func TestCheckPort(t *testing.T) {
|
||||||
|
laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
|
||||||
|
|
||||||
t.Run("tcp_bound", func(t *testing.T) {
|
t.Run("tcp_bound", func(t *testing.T) {
|
||||||
l, err := net.Listen("tcp", "127.0.0.1:")
|
l, err := net.Listen("tcp", laddr.String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.CleanupAndRequireSuccess(t, l.Close)
|
testutil.CleanupAndRequireSuccess(t, l.Close)
|
||||||
|
|
||||||
ipp := netutil.IPPortFromAddr(l.Addr())
|
addr := l.Addr()
|
||||||
require.NotNil(t, ipp)
|
require.IsType(t, new(net.TCPAddr), addr)
|
||||||
require.NotNil(t, ipp.IP)
|
|
||||||
require.NotZero(t, ipp.Port)
|
|
||||||
|
|
||||||
err = CheckPort("tcp", ipp.IP, ipp.Port)
|
ipp := addr.(*net.TCPAddr).AddrPort()
|
||||||
|
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||||
|
require.NotZero(t, ipp.Port())
|
||||||
|
|
||||||
|
err = CheckPort("tcp", ipp)
|
||||||
target := &net.OpError{}
|
target := &net.OpError{}
|
||||||
require.ErrorAs(t, err, &target)
|
require.ErrorAs(t, err, &target)
|
||||||
|
|
||||||
|
@ -216,16 +211,18 @@ func TestCheckPort(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("udp_bound", func(t *testing.T) {
|
t.Run("udp_bound", func(t *testing.T) {
|
||||||
conn, err := net.ListenPacket("udp", "127.0.0.1:")
|
conn, err := net.ListenPacket("udp", laddr.String())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
testutil.CleanupAndRequireSuccess(t, conn.Close)
|
||||||
|
|
||||||
ipp := netutil.IPPortFromAddr(conn.LocalAddr())
|
addr := conn.LocalAddr()
|
||||||
require.NotNil(t, ipp)
|
require.IsType(t, new(net.UDPAddr), addr)
|
||||||
require.NotNil(t, ipp.IP)
|
|
||||||
require.NotZero(t, ipp.Port)
|
|
||||||
|
|
||||||
err = CheckPort("udp", ipp.IP, ipp.Port)
|
ipp := addr.(*net.UDPAddr).AddrPort()
|
||||||
|
require.Equal(t, laddr.Addr(), ipp.Addr())
|
||||||
|
require.NotZero(t, ipp.Port())
|
||||||
|
|
||||||
|
err = CheckPort("udp", ipp)
|
||||||
target := &net.OpError{}
|
target := &net.OpError{}
|
||||||
require.ErrorAs(t, err, &target)
|
require.ErrorAs(t, err, &target)
|
||||||
|
|
||||||
|
@ -233,12 +230,12 @@ func TestCheckPort(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bad_network", func(t *testing.T) {
|
t.Run("bad_network", func(t *testing.T) {
|
||||||
err := CheckPort("bad_network", nil, 0)
|
err := CheckPort("bad_network", netip.AddrPortFrom(netip.Addr{}, 0))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("can_bind", func(t *testing.T) {
|
t.Run("can_bind", func(t *testing.T) {
|
||||||
err := CheckPort("udp", net.IP{0, 0, 0, 0}, 0)
|
err := CheckPort("udp", netip.AddrPortFrom(netip.IPv4Unspecified(), 0))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -322,18 +319,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
|
||||||
`"mtu":1500` +
|
`"mtu":1500` +
|
||||||
`}` + "\n"
|
`}` + "\n"
|
||||||
|
|
||||||
ip4, ip6 := net.IP{1, 2, 3, 4}, net.IP{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
|
||||||
mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen)
|
require.True(t, ok)
|
||||||
|
|
||||||
|
ip6, ok := netip.AddrFromSlice([]byte{0xAA, 0xAA, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||||
|
require.True(t, ok)
|
||||||
|
|
||||||
|
net4 := netip.PrefixFrom(ip4, 24)
|
||||||
|
net6 := netip.PrefixFrom(ip6, 8)
|
||||||
|
|
||||||
iface := &NetInterface{
|
iface := &NetInterface{
|
||||||
Addresses: []net.IP{ip4, ip6},
|
Addresses: []netip.Addr{ip4, ip6},
|
||||||
Subnets: []*net.IPNet{{
|
Subnets: []netip.Prefix{net4, net6},
|
||||||
IP: ip4.Mask(mask4),
|
|
||||||
Mask: mask4,
|
|
||||||
}, {
|
|
||||||
IP: ip6.Mask(mask6),
|
|
||||||
Mask: mask6,
|
|
||||||
}},
|
|
||||||
Name: "iface0",
|
Name: "iface0",
|
||||||
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
|
||||||
Flags: net.FlagUp | net.FlagMulticast,
|
Flags: net.FlagUp | net.FlagMulticast,
|
||||||
|
|
|
@ -15,11 +15,11 @@ import (
|
||||||
// errFSOpen.
|
// errFSOpen.
|
||||||
type errFS struct{}
|
type errFS struct{}
|
||||||
|
|
||||||
// errFSOpen is returned from errGlobFS.Open.
|
// errFSOpen is returned from errFS.Open.
|
||||||
const errFSOpen errors.Error = "test open error"
|
const errFSOpen errors.Error = "test open error"
|
||||||
|
|
||||||
// Open implements the fs.FS interface for *errGlobFS. fsys is always nil and
|
// Open implements the fs.FS interface for *errFS. fsys is always nil and err
|
||||||
// err is always errFSOpen.
|
// is always errFSOpen.
|
||||||
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
|
func (efs *errFS) Open(name string) (fsys fs.File, err error) {
|
||||||
return nil, errFSOpen
|
return nil, errFSOpen
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,11 +175,21 @@ func RootDirFS() (fsys fs.FS) {
|
||||||
return os.DirFS("")
|
return os.DirFS("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NotifyReconfigureSignal notifies c on receiving reconfigure signals.
|
||||||
|
func NotifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
notifyReconfigureSignal(c)
|
||||||
|
}
|
||||||
|
|
||||||
// NotifyShutdownSignal notifies c on receiving shutdown signals.
|
// NotifyShutdownSignal notifies c on receiving shutdown signals.
|
||||||
func NotifyShutdownSignal(c chan<- os.Signal) {
|
func NotifyShutdownSignal(c chan<- os.Signal) {
|
||||||
notifyShutdownSignal(c)
|
notifyShutdownSignal(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsReconfigureSignal returns true if sig is a reconfigure signal.
|
||||||
|
func IsReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return isReconfigureSignal(sig)
|
||||||
|
}
|
||||||
|
|
||||||
// IsShutdownSignal returns true if sig is a shutdown signal.
|
// IsShutdownSignal returns true if sig is a shutdown signal.
|
||||||
func IsShutdownSignal(sig os.Signal) (ok bool) {
|
func IsShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
return isShutdownSignal(sig)
|
return isShutdownSignal(sig)
|
||||||
|
|
|
@ -9,10 +9,18 @@ import (
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
signal.Notify(c, unix.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||||
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
|
signal.Notify(c, unix.SIGINT, unix.SIGQUIT, unix.SIGTERM)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return sig == unix.SIGHUP
|
||||||
|
}
|
||||||
|
|
||||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
switch sig {
|
switch sig {
|
||||||
case
|
case
|
||||||
|
|
|
@ -39,12 +39,20 @@ func isOpenWrt() (ok bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func notifyReconfigureSignal(c chan<- os.Signal) {
|
||||||
|
signal.Notify(c, windows.SIGHUP)
|
||||||
|
}
|
||||||
|
|
||||||
func notifyShutdownSignal(c chan<- os.Signal) {
|
func notifyShutdownSignal(c chan<- os.Signal) {
|
||||||
// syscall.SIGTERM is processed automatically. See go doc os/signal,
|
// syscall.SIGTERM is processed automatically. See go doc os/signal,
|
||||||
// section Windows.
|
// section Windows.
|
||||||
signal.Notify(c, os.Interrupt)
|
signal.Notify(c, os.Interrupt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isReconfigureSignal(sig os.Signal) (ok bool) {
|
||||||
|
return sig == windows.SIGHUP
|
||||||
|
}
|
||||||
|
|
||||||
func isShutdownSignal(sig os.Signal) (ok bool) {
|
func isShutdownSignal(sig os.Signal) (ok bool) {
|
||||||
switch sig {
|
switch sig {
|
||||||
case
|
case
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package aghtest
|
package aghtest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +17,8 @@ import (
|
||||||
|
|
||||||
// Standard Library
|
// Standard Library
|
||||||
|
|
||||||
|
// Package fs
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ fs.FS = &FS{}
|
var _ fs.FS = &FS{}
|
||||||
|
|
||||||
|
@ -58,6 +62,8 @@ func (fsys *StatFS) Stat(name string) (fs.FileInfo, error) {
|
||||||
return fsys.OnStat(name)
|
return fsys.OnStat(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package net
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ net.Listener = (*Listener)(nil)
|
var _ net.Listener = (*Listener)(nil)
|
||||||
|
|
||||||
|
@ -83,31 +89,9 @@ func (l *Listener) Close() (err error) {
|
||||||
return l.OnClose()
|
return l.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module dnsproxy
|
// Module adguard-home
|
||||||
|
|
||||||
// type check
|
// Package aghos
|
||||||
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
|
||||||
|
|
||||||
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
|
||||||
// rename it to just Upstream.
|
|
||||||
type UpstreamMock struct {
|
|
||||||
OnAddress func() (addr string)
|
|
||||||
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
|
||||||
func (u *UpstreamMock) Address() (addr string) {
|
|
||||||
return u.OnAddress()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
|
||||||
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
|
||||||
return u.OnExchange(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module AdGuardHome
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
var _ aghos.FSWatcher = (*FSWatcher)(nil)
|
||||||
|
@ -133,3 +117,59 @@ func (w *FSWatcher) Add(name string) (err error) {
|
||||||
func (w *FSWatcher) Close() (err error) {
|
func (w *FSWatcher) Close() (err error) {
|
||||||
return w.OnClose()
|
return w.OnClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Package agh
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ agh.ServiceWithConfig[struct{}] = (*ServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// ServiceWithConfig is a mock [agh.ServiceWithConfig] implementation for tests.
|
||||||
|
type ServiceWithConfig[ConfigType any] struct {
|
||||||
|
OnStart func() (err error)
|
||||||
|
OnShutdown func(ctx context.Context) (err error)
|
||||||
|
OnConfig func() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Start() (err error) {
|
||||||
|
return s.OnStart()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[_]) Shutdown(ctx context.Context) (err error) {
|
||||||
|
return s.OnShutdown(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [agh.ServiceWithConfig] interface for
|
||||||
|
// *ServiceWithConfig.
|
||||||
|
func (s *ServiceWithConfig[ConfigType]) Config() (c ConfigType) {
|
||||||
|
return s.OnConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module dnsproxy
|
||||||
|
|
||||||
|
// Package upstream
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ upstream.Upstream = (*UpstreamMock)(nil)
|
||||||
|
|
||||||
|
// UpstreamMock is a mock [upstream.Upstream] implementation for tests.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Replace with all uses of Upstream with UpstreamMock and
|
||||||
|
// rename it to just Upstream.
|
||||||
|
type UpstreamMock struct {
|
||||||
|
OnAddress func() (addr string)
|
||||||
|
OnExchange func(req *dns.Msg) (resp *dns.Msg, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||||
|
func (u *UpstreamMock) Address() (addr string) {
|
||||||
|
return u.OnAddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exchange implements the [upstream.Upstream] interface for *UpstreamMock.
|
||||||
|
func (u *UpstreamMock) Exchange(req *dns.Msg) (resp *dns.Msg, err error) {
|
||||||
|
return u.OnExchange(req)
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,3 @@
|
||||||
package aghtest_test
|
package aghtest_test
|
||||||
|
|
||||||
import (
|
// Put interface checks that cause import cycles here.
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// type check
|
|
||||||
var _ aghos.FSWatcher = (*aghtest.FSWatcher)(nil)
|
|
||||||
|
|
|
@ -4,8 +4,46 @@ package aghtls
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// init makes sure that the cipher name map is filled.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Propose a similar API to crypto/tls.
|
||||||
|
func init() {
|
||||||
|
suites := tls.CipherSuites()
|
||||||
|
cipherSuites = make(map[string]uint16, len(suites))
|
||||||
|
for _, s := range suites {
|
||||||
|
cipherSuites[s.Name] = s.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("tls: known ciphers: %q", cipherSuites)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cipherSuites are a name-to-ID mapping of cipher suites from crypto/tls. It
|
||||||
|
// is filled by init. It must not be modified.
|
||||||
|
var cipherSuites map[string]uint16
|
||||||
|
|
||||||
|
// ParseCiphers parses a slice of cipher suites from cipher names.
|
||||||
|
func ParseCiphers(cipherNames []string) (cipherIDs []uint16, err error) {
|
||||||
|
if cipherNames == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherIDs = make([]uint16, 0, len(cipherNames))
|
||||||
|
for _, name := range cipherNames {
|
||||||
|
id, ok := cipherSuites[name]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("unknown cipher %q", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
cipherIDs = append(cipherIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cipherIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SaferCipherSuites returns a set of default cipher suites with vulnerable and
|
// SaferCipherSuites returns a set of default cipher suites with vulnerable and
|
||||||
// weak cipher suites removed.
|
// weak cipher suites removed.
|
||||||
func SaferCipherSuites() (safe []uint16) {
|
func SaferCipherSuites() (safe []uint16) {
|
||||||
|
@ -31,28 +69,3 @@ func SaferCipherSuites() (safe []uint16) {
|
||||||
|
|
||||||
return safe
|
return safe
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseCipherIDs returns a set of cipher suites with the cipher names provided
|
|
||||||
func ParseCipherIDs(ciphers []string) (userCiphers []uint16, err error) {
|
|
||||||
for _, cipher := range ciphers {
|
|
||||||
exists, cipherID := CipherExists(cipher)
|
|
||||||
if exists {
|
|
||||||
userCiphers = append(userCiphers, cipherID)
|
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("unknown cipher : %s ", cipher)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userCiphers, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CipherExists returns cipherid if exists, else return false in boolean
|
|
||||||
func CipherExists(cipher string) (exists bool, cipherID uint16) {
|
|
||||||
for _, s := range tls.CipherSuites() {
|
|
||||||
if s.Name == cipher {
|
|
||||||
return true, s.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, 0
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
package aghtls_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtls"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
aghtest.DiscardLogOutput(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseCiphers(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
want []uint16
|
||||||
|
in []string
|
||||||
|
}{{
|
||||||
|
name: "nil",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: nil,
|
||||||
|
in: nil,
|
||||||
|
}, {
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: []uint16{},
|
||||||
|
in: []string{},
|
||||||
|
}, {}, {
|
||||||
|
name: "one",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: []uint16{tls.TLS_AES_128_GCM_SHA256},
|
||||||
|
in: []string{"TLS_AES_128_GCM_SHA256"},
|
||||||
|
}, {
|
||||||
|
name: "several",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: []uint16{tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384},
|
||||||
|
in: []string{"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384"},
|
||||||
|
}, {
|
||||||
|
name: "bad",
|
||||||
|
wantErrMsg: `unknown cipher "bad_cipher"`,
|
||||||
|
want: nil,
|
||||||
|
in: []string{"bad_cipher"},
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := aghtls.ParseCiphers(tc.in)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package aghtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SystemRootCAs tries to load root certificates from the operating system. It
|
||||||
|
// returns nil in case nothing is found so that Go' crypto/x509 can use its
|
||||||
|
// default algorithm to find system root CA list.
|
||||||
|
//
|
||||||
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/1311.
|
||||||
|
func SystemRootCAs() (roots *x509.CertPool) {
|
||||||
|
return rootCAs()
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package aghtls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rootCAs() (roots *x509.CertPool) {
|
||||||
|
// Directories with the system root certificates, which aren't supported by
|
||||||
|
// Go's crypto/x509.
|
||||||
|
dirs := []string{
|
||||||
|
// Entware.
|
||||||
|
"/opt/etc/ssl/certs",
|
||||||
|
}
|
||||||
|
|
||||||
|
roots = x509.NewCertPool()
|
||||||
|
for _, dir := range dirs {
|
||||||
|
dirEnts, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Improve error handling here and in other places.
|
||||||
|
log.Error("aghtls: opening directory %q: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rootsAdded bool
|
||||||
|
for _, de := range dirEnts {
|
||||||
|
var certData []byte
|
||||||
|
rootFile := filepath.Join(dir, de.Name())
|
||||||
|
certData, err = os.ReadFile(rootFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("aghtls: reading root cert: %s", err)
|
||||||
|
} else {
|
||||||
|
if roots.AppendCertsFromPEM(certData) {
|
||||||
|
rootsAdded = true
|
||||||
|
} else {
|
||||||
|
log.Error("aghtls: could not add root from %q", rootFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rootsAdded {
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package aghtls
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
func rootCAs() (roots *x509.CertPool) {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -3,12 +3,12 @@ package dhcpd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServerConfig is the configuration for the DHCP server. The order of YAML
|
// ServerConfig is the configuration for the DHCP server. The order of YAML
|
||||||
|
@ -65,16 +65,16 @@ type V4ServerConf struct {
|
||||||
Enabled bool `yaml:"-" json:"-"`
|
Enabled bool `yaml:"-" json:"-"`
|
||||||
InterfaceName string `yaml:"-" json:"-"`
|
InterfaceName string `yaml:"-" json:"-"`
|
||||||
|
|
||||||
GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"`
|
GatewayIP netip.Addr `yaml:"gateway_ip" json:"gateway_ip"`
|
||||||
SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"`
|
SubnetMask netip.Addr `yaml:"subnet_mask" json:"subnet_mask"`
|
||||||
// broadcastIP is the broadcasting address pre-calculated from the
|
// broadcastIP is the broadcasting address pre-calculated from the
|
||||||
// configured gateway IP and subnet mask.
|
// configured gateway IP and subnet mask.
|
||||||
broadcastIP net.IP
|
broadcastIP netip.Addr
|
||||||
|
|
||||||
// The first & the last IP address for dynamic leases
|
// The first & the last IP address for dynamic leases
|
||||||
// Bytes [0..2] of the last allowed IP address must match the first IP
|
// Bytes [0..2] of the last allowed IP address must match the first IP
|
||||||
RangeStart net.IP `yaml:"range_start" json:"range_start"`
|
RangeStart netip.Addr `yaml:"range_start" json:"range_start"`
|
||||||
RangeEnd net.IP `yaml:"range_end" json:"range_end"`
|
RangeEnd netip.Addr `yaml:"range_end" json:"range_end"`
|
||||||
|
|
||||||
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
|
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
|
||||||
|
|
||||||
|
@ -95,11 +95,11 @@ type V4ServerConf struct {
|
||||||
ipRange *ipRange
|
ipRange *ipRange
|
||||||
|
|
||||||
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
leaseTime time.Duration // the time during which a dynamic lease is considered valid
|
||||||
dnsIPAddrs []net.IP // IPv4 addresses to return to DHCP clients as DNS server addresses
|
dnsIPAddrs []netip.Addr // IPv4 addresses to return to DHCP clients as DNS server addresses
|
||||||
|
|
||||||
// subnet contains the DHCP server's subnet. The IP is the IP of the
|
// subnet contains the DHCP server's subnet. The IP is the IP of the
|
||||||
// gateway.
|
// gateway.
|
||||||
subnet *net.IPNet
|
subnet netip.Prefix
|
||||||
|
|
||||||
// notify is a way to signal to other components that leases have been
|
// notify is a way to signal to other components that leases have been
|
||||||
// changed. notify must be called outside of locked sections, since the
|
// changed. notify must be called outside of locked sections, since the
|
||||||
|
@ -113,16 +113,12 @@ type V4ServerConf struct {
|
||||||
// errNilConfig is an error returned by validation method if the config is nil.
|
// errNilConfig is an error returned by validation method if the config is nil.
|
||||||
const errNilConfig errors.Error = "nil config"
|
const errNilConfig errors.Error = "nil config"
|
||||||
|
|
||||||
// ensureV4 returns a 4-byte version of ip. An error is returned if the passed
|
// ensureV4 returns an unmapped version of ip. An error is returned if the
|
||||||
// ip is not an IPv4.
|
// passed ip is not an IPv4.
|
||||||
func ensureV4(ip net.IP) (ip4 net.IP, err error) {
|
func ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) {
|
||||||
if ip == nil {
|
ip4 = ip.Unmap()
|
||||||
return nil, fmt.Errorf("%v is not an IP address", ip)
|
if !ip4.IsValid() || !ip4.Is4() {
|
||||||
}
|
return netip.Addr{}, fmt.Errorf("%v is not an IPv4 %s", ip, kind)
|
||||||
|
|
||||||
ip4 = ip.To4()
|
|
||||||
if ip4 == nil {
|
|
||||||
return nil, fmt.Errorf("%v is not an IPv4 address", ip)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ip4, nil
|
return ip4, nil
|
||||||
|
@ -139,33 +135,45 @@ func (c *V4ServerConf) Validate() (err error) {
|
||||||
return errNilConfig
|
return errNilConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
var gatewayIP net.IP
|
gatewayIP, err := ensureV4(c.GatewayIP, "address")
|
||||||
gatewayIP, err = ensureV4(c.GatewayIP)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap an errors since it's inforative enough as is and there is
|
// Don't wrap an errors since it's informative enough as is and there is
|
||||||
// an annotation deferred already.
|
// an annotation deferred already.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.SubnetMask == nil {
|
subnetMask, err := ensureV4(c.SubnetMask, "subnet mask")
|
||||||
return fmt.Errorf("invalid subnet mask: %v", c.SubnetMask)
|
|
||||||
}
|
|
||||||
|
|
||||||
subnetMask := net.IPMask(netutil.CloneIP(c.SubnetMask.To4()))
|
|
||||||
c.subnet = &net.IPNet{
|
|
||||||
IP: gatewayIP,
|
|
||||||
Mask: subnetMask,
|
|
||||||
}
|
|
||||||
c.broadcastIP = aghnet.BroadcastFromIPNet(c.subnet)
|
|
||||||
|
|
||||||
c.ipRange, err = newIPRange(c.RangeStart, c.RangeEnd)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Don't wrap an errors since it's inforative enough as is and there is
|
// Don't wrap an errors since it's informative enough as is and there is
|
||||||
|
// an annotation deferred already.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
maskLen, _ := net.IPMask(subnetMask.AsSlice()).Size()
|
||||||
|
|
||||||
|
c.subnet = netip.PrefixFrom(gatewayIP, maskLen)
|
||||||
|
c.broadcastIP = aghnet.BroadcastFromPref(c.subnet)
|
||||||
|
|
||||||
|
rangeStart, err := ensureV4(c.RangeStart, "address")
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap an errors since it's informative enough as is and there is
|
||||||
|
// an annotation deferred already.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rangeEnd, err := ensureV4(c.RangeEnd, "address")
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap an errors since it's informative enough as is and there is
|
||||||
// an annotation deferred already.
|
// an annotation deferred already.
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.ipRange.contains(gatewayIP) {
|
c.ipRange, err = newIPRange(rangeStart.AsSlice(), rangeEnd.AsSlice())
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap an errors since it's informative enough as is and there is
|
||||||
|
// an annotation deferred already.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ipRange.contains(gatewayIP.AsSlice()) {
|
||||||
return fmt.Errorf("gateway ip %v in the ip range: %v-%v",
|
return fmt.Errorf("gateway ip %v in the ip range: %v-%v",
|
||||||
gatewayIP,
|
gatewayIP,
|
||||||
c.RangeStart,
|
c.RangeStart,
|
||||||
|
@ -173,14 +181,14 @@ func (c *V4ServerConf) Validate() (err error) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.subnet.Contains(c.RangeStart) {
|
if !c.subnet.Contains(rangeStart) {
|
||||||
return fmt.Errorf("range start %v is outside network %v",
|
return fmt.Errorf("range start %v is outside network %v",
|
||||||
c.RangeStart,
|
c.RangeStart,
|
||||||
c.subnet,
|
c.subnet,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.subnet.Contains(c.RangeEnd) {
|
if !c.subnet.Contains(rangeEnd) {
|
||||||
return fmt.Errorf("range end %v is outside network %v",
|
return fmt.Errorf("range end %v is outside network %v",
|
||||||
c.RangeEnd,
|
c.RangeEnd,
|
||||||
c.subnet,
|
c.subnet,
|
||||||
|
|
|
@ -73,10 +73,10 @@ func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err erro
|
||||||
|
|
||||||
return &dhcpConn{
|
return &dhcpConn{
|
||||||
udpConn: bcast,
|
udpConn: bcast,
|
||||||
bcastIP: s.conf.broadcastIP,
|
bcastIP: s.conf.broadcastIP.AsSlice(),
|
||||||
rawConn: ucast,
|
rawConn: ucast,
|
||||||
srcMAC: iface.HardwareAddr,
|
srcMAC: iface.HardwareAddr,
|
||||||
srcIP: s.conf.dnsIPAddrs[0],
|
srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -242,7 +242,7 @@ func Create(conf *ServerConfig) (s *server, err error) {
|
||||||
v4conf := conf.Conf4
|
v4conf := conf.Conf4
|
||||||
v4conf.InterfaceName = s.conf.InterfaceName
|
v4conf.InterfaceName = s.conf.InterfaceName
|
||||||
v4conf.notify = s.onNotify
|
v4conf.notify = s.onNotify
|
||||||
v4conf.Enabled = s.conf.Enabled && len(v4conf.RangeStart) != 0
|
v4conf.Enabled = s.conf.Enabled && v4conf.RangeStart.IsValid()
|
||||||
|
|
||||||
s.srv4, err = v4Create(&v4conf)
|
s.srv4, err = v4Create(&v4conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,6 +4,7 @@ package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -33,10 +34,10 @@ func TestDB(t *testing.T) {
|
||||||
|
|
||||||
s.srv4, err = v4Create(&V4ServerConf{
|
s.srv4, err = v4Create(&V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 100},
|
RangeStart: netip.MustParseAddr("192.168.10.100"),
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: netip.MustParseAddr("192.168.10.200"),
|
||||||
GatewayIP: net.IP{192, 168, 10, 1},
|
GatewayIP: netip.MustParseAddr("192.168.10.1"),
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||||
notify: testNotify,
|
notify: testNotify,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -113,35 +114,35 @@ func TestNormalizeLeases(t *testing.T) {
|
||||||
func TestV4Server_badRange(t *testing.T) {
|
func TestV4Server_badRange(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
gatewayIP netip.Addr
|
||||||
|
subnetMask netip.Addr
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
gatewayIP net.IP
|
|
||||||
subnetMask net.IP
|
|
||||||
}{{
|
}{{
|
||||||
name: "gateway_in_range",
|
name: "gateway_in_range",
|
||||||
|
gatewayIP: netip.MustParseAddr("192.168.10.120"),
|
||||||
|
subnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||||
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
|
wantErrMsg: "dhcpv4: gateway ip 192.168.10.120 in the ip range: " +
|
||||||
"192.168.10.20-192.168.10.200",
|
"192.168.10.20-192.168.10.200",
|
||||||
gatewayIP: net.IP{192, 168, 10, 120},
|
|
||||||
subnetMask: net.IP{255, 255, 255, 0},
|
|
||||||
}, {
|
}, {
|
||||||
name: "outside_range_start",
|
name: "outside_range_start",
|
||||||
|
gatewayIP: netip.MustParseAddr("192.168.10.1"),
|
||||||
|
subnetMask: netip.MustParseAddr("255.255.255.240"),
|
||||||
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
|
wantErrMsg: "dhcpv4: range start 192.168.10.20 is outside network " +
|
||||||
"192.168.10.1/28",
|
"192.168.10.1/28",
|
||||||
gatewayIP: net.IP{192, 168, 10, 1},
|
|
||||||
subnetMask: net.IP{255, 255, 255, 240},
|
|
||||||
}, {
|
}, {
|
||||||
name: "outside_range_end",
|
name: "outside_range_end",
|
||||||
|
gatewayIP: netip.MustParseAddr("192.168.10.1"),
|
||||||
|
subnetMask: netip.MustParseAddr("255.255.255.224"),
|
||||||
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
|
wantErrMsg: "dhcpv4: range end 192.168.10.200 is outside network " +
|
||||||
"192.168.10.1/27",
|
"192.168.10.1/27",
|
||||||
gatewayIP: net.IP{192, 168, 10, 1},
|
|
||||||
subnetMask: net.IP{255, 255, 255, 224},
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
conf := V4ServerConf{
|
conf := V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
RangeStart: net.IP{192, 168, 10, 20},
|
RangeStart: netip.MustParseAddr("192.168.10.20"),
|
||||||
RangeEnd: net.IP{192, 168, 10, 200},
|
RangeEnd: netip.MustParseAddr("192.168.10.200"),
|
||||||
GatewayIP: tc.gatewayIP,
|
GatewayIP: tc.gatewayIP,
|
||||||
SubnetMask: tc.subnetMask,
|
SubnetMask: tc.subnetMask,
|
||||||
notify: testNotify,
|
notify: testNotify,
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
|
||||||
|
@ -17,11 +18,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type v4ServerConfJSON struct {
|
type v4ServerConfJSON struct {
|
||||||
GatewayIP net.IP `json:"gateway_ip"`
|
GatewayIP netip.Addr `json:"gateway_ip"`
|
||||||
SubnetMask net.IP `json:"subnet_mask"`
|
SubnetMask netip.Addr `json:"subnet_mask"`
|
||||||
RangeStart net.IP `json:"range_start"`
|
RangeStart netip.Addr `json:"range_start"`
|
||||||
RangeEnd net.IP `json:"range_end"`
|
RangeEnd netip.Addr `json:"range_end"`
|
||||||
LeaseDuration uint32 `json:"lease_duration"`
|
LeaseDuration uint32 `json:"lease_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
|
func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
|
||||||
|
@ -39,8 +40,8 @@ func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
|
||||||
}
|
}
|
||||||
|
|
||||||
type v6ServerConfJSON struct {
|
type v6ServerConfJSON struct {
|
||||||
RangeStart net.IP `json:"range_start"`
|
RangeStart netip.Addr `json:"range_start"`
|
||||||
LeaseDuration uint32 `json:"lease_duration"`
|
LeaseDuration uint32 `json:"lease_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
||||||
|
@ -49,7 +50,7 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
|
||||||
}
|
}
|
||||||
|
|
||||||
return V6ServerConf{
|
return V6ServerConf{
|
||||||
RangeStart: j.RangeStart,
|
RangeStart: j.RangeStart.AsSlice(),
|
||||||
LeaseDuration: j.LeaseDuration,
|
LeaseDuration: j.LeaseDuration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,18 +79,7 @@ func (s *server) handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
status.Leases = s.Leases(LeasesDynamic)
|
status.Leases = s.Leases(LeasesDynamic)
|
||||||
status.StaticLeases = s.Leases(LeasesStatic)
|
status.StaticLeases = s.Leases(LeasesStatic)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, status)
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(status)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(
|
|
||||||
r,
|
|
||||||
w,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
"Unable to marshal DHCP status json: %s",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
func (s *server) enableDHCP(ifaceName string) (code int, err error) {
|
||||||
|
@ -155,7 +145,7 @@ func (s *server) handleDHCPSetConfigV4(
|
||||||
|
|
||||||
v4Conf := conf.V4.toServerConf()
|
v4Conf := conf.V4.toServerConf()
|
||||||
v4Conf.Enabled = conf.Enabled == aghalg.NBTrue
|
v4Conf.Enabled = conf.Enabled == aghalg.NBTrue
|
||||||
if len(v4Conf.RangeStart) == 0 {
|
if !v4Conf.RangeStart.IsValid() {
|
||||||
v4Conf.Enabled = false
|
v4Conf.Enabled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,22 +236,7 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if conf.Enabled != aghalg.NBNull {
|
s.setConfFromJSON(conf, srv4, srv6)
|
||||||
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.InterfaceName != "" {
|
|
||||||
s.conf.InterfaceName = conf.InterfaceName
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv4 != nil {
|
|
||||||
s.srv4 = srv4
|
|
||||||
}
|
|
||||||
|
|
||||||
if srv6 != nil {
|
|
||||||
s.srv6 = srv6
|
|
||||||
}
|
|
||||||
|
|
||||||
s.conf.ConfigModified()
|
s.conf.ConfigModified()
|
||||||
|
|
||||||
err = s.dbLoad()
|
err = s.dbLoad()
|
||||||
|
@ -280,13 +255,33 @@ func (s *server) handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setConfFromJSON sets configuration parameters in s from the new configuration
|
||||||
|
// decoded from JSON.
|
||||||
|
func (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServer) {
|
||||||
|
if conf.Enabled != aghalg.NBNull {
|
||||||
|
s.conf.Enabled = conf.Enabled == aghalg.NBTrue
|
||||||
|
}
|
||||||
|
|
||||||
|
if conf.InterfaceName != "" {
|
||||||
|
s.conf.InterfaceName = conf.InterfaceName
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv4 != nil {
|
||||||
|
s.srv4 = srv4
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv6 != nil {
|
||||||
|
s.srv6 = srv6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type netInterfaceJSON struct {
|
type netInterfaceJSON struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
HardwareAddr string `json:"hardware_address"`
|
HardwareAddr string `json:"hardware_address"`
|
||||||
Flags string `json:"flags"`
|
Flags string `json:"flags"`
|
||||||
GatewayIP net.IP `json:"gateway_ip"`
|
GatewayIP netip.Addr `json:"gateway_ip"`
|
||||||
Addrs4 []net.IP `json:"ipv4_addresses"`
|
Addrs4 []netip.Addr `json:"ipv4_addresses"`
|
||||||
Addrs6 []net.IP `json:"ipv6_addresses"`
|
Addrs6 []netip.Addr `json:"ipv6_addresses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -347,13 +342,18 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// ignore link-local
|
// ignore link-local
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Try to listen DHCP on LLA as well.
|
||||||
if ipnet.IP.IsLinkLocalUnicast() {
|
if ipnet.IP.IsLinkLocalUnicast() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ipnet.IP.To4() != nil {
|
|
||||||
jsonIface.Addrs4 = append(jsonIface.Addrs4, ipnet.IP)
|
if ip4 := ipnet.IP.To4(); ip4 != nil {
|
||||||
|
addr := netip.AddrFrom4(*(*[4]byte)(ip4))
|
||||||
|
jsonIface.Addrs4 = append(jsonIface.Addrs4, addr)
|
||||||
} else {
|
} else {
|
||||||
jsonIface.Addrs6 = append(jsonIface.Addrs6, ipnet.IP)
|
addr := netip.AddrFrom16(*(*[16]byte)(ipnet.IP))
|
||||||
|
jsonIface.Addrs6 = append(jsonIface.Addrs6, addr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
|
if len(jsonIface.Addrs4)+len(jsonIface.Addrs6) != 0 {
|
||||||
|
|
|
@ -3,11 +3,10 @@
|
||||||
package dhcpd
|
package dhcpd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// jsonError is a generic JSON error response.
|
// jsonError is a generic JSON error response.
|
||||||
|
@ -25,15 +24,9 @@ type jsonError struct {
|
||||||
// TODO(a.garipov): Either take the logger from the server after we've
|
// TODO(a.garipov): Either take the logger from the server after we've
|
||||||
// refactored logging or make this not a method of *Server.
|
// refactored logging or make this not a method of *Server.
|
||||||
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
func (s *server) notImplemented(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponseCode(w, r, http.StatusNotImplemented, &jsonError{
|
||||||
w.WriteHeader(http.StatusNotImplemented)
|
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(&jsonError{
|
|
||||||
Message: aghos.Unsupported("dhcp").Error(),
|
Message: aghos.Unsupported("dhcp").Error(),
|
||||||
})
|
})
|
||||||
if err != nil {
|
|
||||||
log.Debug("writing 501 json response: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerHandlers sets the handlers for DHCP HTTP API that always respond with
|
// registerHandlers sets the handlers for DHCP HTTP API that always respond with
|
||||||
|
|
|
@ -27,6 +27,8 @@ const maxRangeLen = math.MaxUint32
|
||||||
|
|
||||||
// newIPRange creates a new IP address range. start must be less than end. The
|
// newIPRange creates a new IP address range. start must be less than end. The
|
||||||
// resulting range must not be greater than maxRangeLen.
|
// resulting range must not be greater than maxRangeLen.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use netip.Addr.
|
||||||
func newIPRange(start, end net.IP) (r *ipRange, err error) {
|
func newIPRange(start, end net.IP) (r *ipRange, err error) {
|
||||||
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()
|
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()
|
||||||
|
|
||||||
|
|
|
@ -372,12 +372,9 @@ func (s *v4Server) prepareOptions() {
|
||||||
dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),
|
dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),
|
||||||
|
|
||||||
// Values From Configuration
|
// Values From Configuration
|
||||||
|
dhcpv4.OptRouter(s.conf.GatewayIP.AsSlice()),
|
||||||
|
|
||||||
// Set the Router Option to working subnet's IP since it's initialized
|
dhcpv4.OptSubnetMask(s.conf.SubnetMask.AsSlice()),
|
||||||
// with the address of the gateway.
|
|
||||||
dhcpv4.OptRouter(s.conf.subnet.IP),
|
|
||||||
|
|
||||||
dhcpv4.OptSubnetMask(s.conf.subnet.Mask),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set values for explicitly configured options.
|
// Set values for explicitly configured options.
|
||||||
|
|
|
@ -251,8 +251,6 @@ func TestPrepareOptions(t *testing.T) {
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
s := &v4Server{
|
s := &v4Server{
|
||||||
conf: &V4ServerConf{
|
conf: &V4ServerConf{
|
||||||
// Just to avoid nil pointer dereference.
|
|
||||||
subnet: &net.IPNet{},
|
|
||||||
Options: tc.opts,
|
Options: tc.opts,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -295,7 +296,8 @@ func (s *v4Server) addLease(l *Lease) (err error) {
|
||||||
if l.IsStatic() {
|
if l.IsStatic() {
|
||||||
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
|
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
|
||||||
// disabled.
|
// disabled.
|
||||||
if sn := s.conf.subnet; !sn.Contains(l.IP) {
|
addr := netip.AddrFrom4(*(*[4]byte)(l.IP.To4()))
|
||||||
|
if sn := s.conf.subnet; !sn.Contains(addr) {
|
||||||
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
|
return fmt.Errorf("subnet %s does not contain the ip %q", sn, l.IP)
|
||||||
}
|
}
|
||||||
} else if !inOffset {
|
} else if !inOffset {
|
||||||
|
@ -353,7 +355,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
|
||||||
ip := l.IP.To4()
|
ip := l.IP.To4()
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
|
return fmt.Errorf("invalid ip %q, only ipv4 is supported", l.IP)
|
||||||
} else if gwIP := s.conf.GatewayIP; gwIP.Equal(ip) {
|
} else if gwIP := s.conf.GatewayIP; gwIP == netip.AddrFrom4(*(*[4]byte)(ip)) {
|
||||||
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
|
return fmt.Errorf("can't assign the gateway IP %s to the lease", gwIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -701,7 +703,8 @@ func (s *v4Server) handleSelecting(
|
||||||
// Client inserts the address of the selected server in server identifier,
|
// Client inserts the address of the selected server in server identifier,
|
||||||
// ciaddr MUST be zero.
|
// ciaddr MUST be zero.
|
||||||
mac := req.ClientHWAddr
|
mac := req.ClientHWAddr
|
||||||
if !sid.Equal(s.conf.dnsIPAddrs[0]) {
|
|
||||||
|
if !sid.Equal(s.conf.dnsIPAddrs[0].AsSlice()) {
|
||||||
log.Debug("dhcpv4: bad server identifier in req msg for %s: %s", mac, sid)
|
log.Debug("dhcpv4: bad server identifier in req msg for %s: %s", mac, sid)
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
|
@ -733,7 +736,8 @@ func (s *v4Server) handleSelecting(
|
||||||
func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) {
|
func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease, needsReply bool) {
|
||||||
mac := req.ClientHWAddr
|
mac := req.ClientHWAddr
|
||||||
|
|
||||||
if ip4 := reqIP.To4(); ip4 == nil {
|
ip4 := reqIP.To4()
|
||||||
|
if ip4 == nil {
|
||||||
log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP)
|
log.Debug("dhcpv4: bad requested address in req msg for %s: %s", mac, reqIP)
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
|
@ -747,7 +751,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease,
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !s.conf.subnet.Contains(reqIP) {
|
if !s.conf.subnet.Contains(netip.AddrFrom4(*(*[4]byte)(ip4))) {
|
||||||
// If the DHCP server detects that the client is on the wrong net then
|
// If the DHCP server detects that the client is on the wrong net then
|
||||||
// the server SHOULD send a DHCPNAK message to the client.
|
// the server SHOULD send a DHCPNAK message to the client.
|
||||||
log.Debug("dhcpv4: wrong subnet in init-reboot req msg for %s: %s", mac, reqIP)
|
log.Debug("dhcpv4: wrong subnet in init-reboot req msg for %s: %s", mac, reqIP)
|
||||||
|
@ -972,7 +976,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
|
||||||
// Include server's identifier option since any reply should contain it.
|
// Include server's identifier option since any reply should contain it.
|
||||||
//
|
//
|
||||||
// See https://datatracker.ietf.org/doc/html/rfc2131#page-29.
|
// See https://datatracker.ietf.org/doc/html/rfc2131#page-29.
|
||||||
resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0]))
|
resp.UpdateOption(dhcpv4.OptServerIdentifier(s.conf.dnsIPAddrs[0].AsSlice()))
|
||||||
|
|
||||||
// TODO(a.garipov): Refactor this into handlers.
|
// TODO(a.garipov): Refactor this into handlers.
|
||||||
var l *Lease
|
var l *Lease
|
||||||
|
@ -1188,7 +1192,14 @@ func (s *v4Server) Start() (err error) {
|
||||||
s.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...))
|
s.implicitOpts.Update(dhcpv4.OptDNS(dnsIPAddrs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
s.conf.dnsIPAddrs = dnsIPAddrs
|
for _, ip := range dnsIPAddrs {
|
||||||
|
ip = ip.To4()
|
||||||
|
if ip == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conf.dnsIPAddrs = append(s.conf.dnsIPAddrs, netip.AddrFrom4(*(*[4]byte)(ip)))
|
||||||
|
}
|
||||||
|
|
||||||
var c net.PacketConn
|
var c net.PacketConn
|
||||||
if c, err = s.newDHCPConn(iface); err != nil {
|
if c, err = s.newDHCPConn(iface); err != nil {
|
||||||
|
|
|
@ -5,6 +5,7 @@ package dhcpd
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -22,11 +23,11 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
DefaultRangeStart = net.IP{192, 168, 10, 100}
|
DefaultRangeStart = netip.MustParseAddr("192.168.10.100")
|
||||||
DefaultRangeEnd = net.IP{192, 168, 10, 200}
|
DefaultRangeEnd = netip.MustParseAddr("192.168.10.200")
|
||||||
DefaultGatewayIP = net.IP{192, 168, 10, 1}
|
DefaultGatewayIP = netip.MustParseAddr("192.168.10.1")
|
||||||
DefaultSelfIP = net.IP{192, 168, 10, 2}
|
DefaultSelfIP = netip.MustParseAddr("192.168.10.2")
|
||||||
DefaultSubnetMask = net.IP{255, 255, 255, 0}
|
DefaultSubnetMask = netip.MustParseAddr("255.255.255.0")
|
||||||
)
|
)
|
||||||
|
|
||||||
// defaultV4ServerConf returns the default configuration for *v4Server to use in
|
// defaultV4ServerConf returns the default configuration for *v4Server to use in
|
||||||
|
@ -39,7 +40,7 @@ func defaultV4ServerConf() (conf *V4ServerConf) {
|
||||||
GatewayIP: DefaultGatewayIP,
|
GatewayIP: DefaultGatewayIP,
|
||||||
SubnetMask: DefaultSubnetMask,
|
SubnetMask: DefaultSubnetMask,
|
||||||
notify: testNotify,
|
notify: testNotify,
|
||||||
dnsIPAddrs: []net.IP{DefaultSelfIP},
|
dnsIPAddrs: []netip.Addr{DefaultSelfIP},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +83,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Expiry: time.Unix(leaseExpireStatic, 0),
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
Hostname: staticName,
|
Hostname: staticName,
|
||||||
HWAddr: anotherMAC,
|
HWAddr: anotherMAC,
|
||||||
IP: anotherIP,
|
IP: anotherIP.AsSlice(),
|
||||||
})
|
})
|
||||||
assert.ErrorIs(t, err, ErrDupHostname)
|
assert.ErrorIs(t, err, ErrDupHostname)
|
||||||
})
|
})
|
||||||
|
@ -96,7 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
Expiry: time.Unix(leaseExpireStatic, 0),
|
Expiry: time.Unix(leaseExpireStatic, 0),
|
||||||
Hostname: anotherName,
|
Hostname: anotherName,
|
||||||
HWAddr: staticMAC,
|
HWAddr: staticMAC,
|
||||||
IP: anotherIP,
|
IP: anotherIP.AsSlice(),
|
||||||
})
|
})
|
||||||
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
testutil.AssertErrorMsg(t, wantErrMsg, err)
|
||||||
})
|
})
|
||||||
|
@ -135,7 +136,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
|
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
|
||||||
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),
|
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),
|
||||||
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),
|
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),
|
||||||
dhcpv4.WithGatewayIP(DefaultGatewayIP),
|
dhcpv4.WithGatewayIP(DefaultGatewayIP.AsSlice()),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("same_name", func(t *testing.T) {
|
t.Run("same_name", func(t *testing.T) {
|
||||||
resp := discoverAnOffer(t, staticName, anotherIP, anotherMAC)
|
resp := discoverAnOffer(t, staticName, anotherIP.AsSlice(), anotherMAC)
|
||||||
|
|
||||||
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
||||||
dhcpv4.OptHostName(staticName),
|
dhcpv4.OptHostName(staticName),
|
||||||
|
@ -164,7 +165,7 @@ func TestV4Server_leasing(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("same_mac", func(t *testing.T) {
|
t.Run("same_mac", func(t *testing.T) {
|
||||||
resp := discoverAnOffer(t, anotherName, anotherIP, staticMAC)
|
resp := discoverAnOffer(t, anotherName, anotherIP.AsSlice(), staticMAC)
|
||||||
|
|
||||||
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
req, err := dhcpv4.NewRequestFromOffer(resp, dhcpv4.WithOption(
|
||||||
dhcpv4.OptHostName(anotherName),
|
dhcpv4.OptHostName(anotherName),
|
||||||
|
@ -219,7 +220,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
|
||||||
lease: &Lease{
|
lease: &Lease{
|
||||||
Hostname: "probably-router.local",
|
Hostname: "probably-router.local",
|
||||||
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
|
||||||
IP: DefaultGatewayIP,
|
IP: DefaultGatewayIP.AsSlice(),
|
||||||
},
|
},
|
||||||
name: "with_gateway_ip",
|
name: "with_gateway_ip",
|
||||||
wantErrMsg: "dhcpv4: adding static lease: " +
|
wantErrMsg: "dhcpv4: adding static lease: " +
|
||||||
|
@ -326,7 +327,7 @@ func TestV4_AddReplace(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestV4Server_handle_optionsPriority(t *testing.T) {
|
func TestV4Server_handle_optionsPriority(t *testing.T) {
|
||||||
defaultIP := net.IP{192, 168, 1, 1}
|
defaultIP := netip.MustParseAddr("192.168.1.1")
|
||||||
knownIP := net.IP{1, 2, 3, 4}
|
knownIP := net.IP{1, 2, 3, 4}
|
||||||
|
|
||||||
// prepareSrv creates a *v4Server and sets the opt6IPs in the initial
|
// prepareSrv creates a *v4Server and sets the opt6IPs in the initial
|
||||||
|
@ -343,14 +344,14 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
|
||||||
}
|
}
|
||||||
conf.Options = []string{b.String()}
|
conf.Options = []string{b.String()}
|
||||||
} else {
|
} else {
|
||||||
defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }()
|
defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP.AsSlice())) }()
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
s, err = v4Create(conf)
|
s, err = v4Create(conf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{defaultIP}
|
s.conf.dnsIPAddrs = []netip.Addr{defaultIP}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
@ -386,7 +387,7 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
|
||||||
t.Run("default", func(t *testing.T) {
|
t.Run("default", func(t *testing.T) {
|
||||||
s := prepareSrv(t, nil)
|
s := prepareSrv(t, nil)
|
||||||
|
|
||||||
checkResp(t, s, []net.IP{defaultIP})
|
checkResp(t, s, []net.IP{defaultIP.AsSlice()})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("explicitly_configured", func(t *testing.T) {
|
t.Run("explicitly_configured", func(t *testing.T) {
|
||||||
|
@ -506,8 +507,9 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
s, ok := sIface.(*v4Server)
|
s, ok := sIface.(*v4Server)
|
||||||
require.True(t, ok)
|
require.True(t, ok)
|
||||||
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
dnsAddr := netip.MustParseAddr("192.168.10.1")
|
||||||
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
|
s.conf.dnsIPAddrs = []netip.Addr{dnsAddr}
|
||||||
|
s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice()))
|
||||||
|
|
||||||
l := &Lease{
|
l := &Lease{
|
||||||
Hostname: "static-1.local",
|
Hostname: "static-1.local",
|
||||||
|
@ -539,9 +541,12 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
|
ones, _ := resp.SubnetMask().Size()
|
||||||
|
assert.Equal(t, s.conf.subnet.Bits(), ones)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -561,16 +566,19 @@ func TestV4StaticLease_Get(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
assert.True(t, l.IP.Equal(resp.YourIPAddr))
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(resp.Router()[0]))
|
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
|
ones, _ := resp.SubnetMask().Size()
|
||||||
|
assert.Equal(t, s.conf.subnet.Bits(), ones)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
dnsAddrs := resp.DNS()
|
dnsAddrs := resp.DNS()
|
||||||
require.Len(t, dnsAddrs, 1)
|
require.Len(t, dnsAddrs, 1)
|
||||||
|
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(dnsAddrs[0]))
|
assert.True(t, dnsAddrs[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
t.Run("check_lease", func(t *testing.T) {
|
t.Run("check_lease", func(t *testing.T) {
|
||||||
ls := s.GetLeases(LeasesStatic)
|
ls := s.GetLeases(LeasesStatic)
|
||||||
|
@ -591,8 +599,8 @@ func TestV4DynamicLease_Get(t *testing.T) {
|
||||||
s, err := v4Create(conf)
|
s, err := v4Create(conf)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
|
s.conf.dnsIPAddrs = []netip.Addr{netip.MustParseAddr("192.168.10.1")}
|
||||||
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
|
s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs[0].AsSlice()))
|
||||||
|
|
||||||
var req, resp *dhcpv4.DHCPv4
|
var req, resp *dhcpv4.DHCPv4
|
||||||
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
mac := net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
|
||||||
|
@ -617,15 +625,16 @@ func TestV4DynamicLease_Get(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
|
|
||||||
assert.Equal(t, s.conf.RangeStart, resp.YourIPAddr)
|
assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))
|
||||||
assert.Equal(t, s.conf.GatewayIP, resp.ServerIdentifier())
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
router := resp.Router()
|
router := resp.Router()
|
||||||
require.Len(t, router, 1)
|
require.Len(t, router, 1)
|
||||||
|
|
||||||
assert.Equal(t, s.conf.GatewayIP, router[0])
|
assert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
|
ones, _ := resp.SubnetMask().Size()
|
||||||
|
assert.Equal(t, s.conf.subnet.Bits(), ones)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||||
assert.Equal(t, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN))
|
assert.Equal(t, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN))
|
||||||
|
|
||||||
|
@ -649,15 +658,17 @@ func TestV4DynamicLease_Get(t *testing.T) {
|
||||||
t.Run("ack", func(t *testing.T) {
|
t.Run("ack", func(t *testing.T) {
|
||||||
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
|
||||||
assert.Equal(t, mac, resp.ClientHWAddr)
|
assert.Equal(t, mac, resp.ClientHWAddr)
|
||||||
assert.True(t, s.conf.RangeStart.Equal(resp.YourIPAddr))
|
assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))
|
||||||
|
|
||||||
router := resp.Router()
|
router := resp.Router()
|
||||||
require.Len(t, router, 1)
|
require.Len(t, router, 1)
|
||||||
|
|
||||||
assert.Equal(t, s.conf.GatewayIP, router[0])
|
assert.True(t, router[0].Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
|
|
||||||
assert.True(t, s.conf.GatewayIP.Equal(resp.ServerIdentifier()))
|
assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
|
||||||
assert.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
|
|
||||||
|
ones, _ := resp.SubnetMask().Size()
|
||||||
|
assert.Equal(t, s.conf.subnet.Bits(), ones)
|
||||||
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
assert.Equal(t, s.conf.leaseTime.Seconds(), resp.IPAddressLeaseTime(-1).Seconds())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ import (
|
||||||
// accessCtx controls IP and client blocking that takes place before all other
|
// accessCtx controls IP and client blocking that takes place before all other
|
||||||
// processing. An accessCtx is safe for concurrent use.
|
// processing. An accessCtx is safe for concurrent use.
|
||||||
type accessCtx struct {
|
type accessCtx struct {
|
||||||
|
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
|
||||||
allowedIPs *netutil.IPMap
|
allowedIPs *netutil.IPMap
|
||||||
blockedIPs *netutil.IPMap
|
blockedIPs *netutil.IPMap
|
||||||
|
|
||||||
|
|
|
@ -123,7 +123,14 @@ type quicConnection interface {
|
||||||
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
|
func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string, err error) {
|
||||||
proto := pctx.Proto
|
proto := pctx.Proto
|
||||||
if proto == proxy.ProtoHTTPS {
|
if proto == proxy.ProtoHTTPS {
|
||||||
return clientIDFromDNSContextHTTPS(pctx)
|
clientID, err = clientIDFromDNSContextHTTPS(pctx)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("checking url: %w", err)
|
||||||
|
} else if clientID != "" {
|
||||||
|
return clientID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go on and check the domain name as well.
|
||||||
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
} else if proto != proxy.ProtoTLS && proto != proxy.ProtoQUIC {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
@ -133,31 +140,9 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cliSrvName := ""
|
cliSrvName, err := clientServerName(pctx, proto)
|
||||||
switch proto {
|
if err != nil {
|
||||||
case proxy.ProtoTLS:
|
return "", err
|
||||||
conn := pctx.Conn
|
|
||||||
tc, ok := conn.(tlsConn)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"proxy ctx conn of proto %s is %T, want *tls.Conn",
|
|
||||||
proto,
|
|
||||||
conn,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliSrvName = tc.ConnectionState().ServerName
|
|
||||||
case proxy.ProtoQUIC:
|
|
||||||
conn, ok := pctx.QUICConnection.(quicConnection)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
|
|
||||||
proto,
|
|
||||||
pctx.QUICConnection,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliSrvName = conn.ConnectionState().TLS.ServerName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clientID, err = clientIDFromClientServerName(
|
clientID, err = clientIDFromClientServerName(
|
||||||
|
@ -171,3 +156,35 @@ func (s *Server) clientIDFromDNSContext(pctx *proxy.DNSContext) (clientID string
|
||||||
|
|
||||||
return clientID, nil
|
return clientID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clientServerName returns the TLS server name based on the protocol.
|
||||||
|
func clientServerName(pctx *proxy.DNSContext, proto proxy.Proto) (srvName string, err error) {
|
||||||
|
switch proto {
|
||||||
|
case proxy.ProtoHTTPS:
|
||||||
|
if connState := pctx.HTTPRequest.TLS; connState != nil {
|
||||||
|
srvName = pctx.HTTPRequest.TLS.ServerName
|
||||||
|
}
|
||||||
|
case proxy.ProtoQUIC:
|
||||||
|
qConn := pctx.QUICConnection
|
||||||
|
conn, ok := qConn.(quicConnection)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"proxy ctx quic conn of proto %s is %T, want quic.Connection",
|
||||||
|
proto,
|
||||||
|
qConn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvName = conn.ConnectionState().TLS.ServerName
|
||||||
|
case proxy.ProtoTLS:
|
||||||
|
conn := pctx.Conn
|
||||||
|
tc, ok := conn.(tlsConn)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("proxy ctx conn of proto %s is %T, want *tls.Conn", proto, conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
srvName = tc.ConnectionState().ServerName
|
||||||
|
}
|
||||||
|
|
||||||
|
return srvName, nil
|
||||||
|
}
|
||||||
|
|
|
@ -160,6 +160,22 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||||
wantClientID: "insensitive",
|
wantClientID: "insensitive",
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
strictSNI: true,
|
strictSNI: true,
|
||||||
|
}, {
|
||||||
|
name: "https_no_clientid",
|
||||||
|
proto: proxy.ProtoHTTPS,
|
||||||
|
hostSrvName: "example.com",
|
||||||
|
cliSrvName: "example.com",
|
||||||
|
wantClientID: "",
|
||||||
|
wantErrMsg: "",
|
||||||
|
strictSNI: true,
|
||||||
|
}, {
|
||||||
|
name: "https_clientid",
|
||||||
|
proto: proxy.ProtoHTTPS,
|
||||||
|
hostSrvName: "example.com",
|
||||||
|
cliSrvName: "cli.example.com",
|
||||||
|
wantClientID: "cli",
|
||||||
|
wantErrMsg: "",
|
||||||
|
strictSNI: true,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
|
@ -173,16 +189,32 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||||
conf: ServerConfig{TLSConfig: tlsConf},
|
conf: ServerConfig{TLSConfig: tlsConf},
|
||||||
}
|
}
|
||||||
|
|
||||||
var conn net.Conn
|
var (
|
||||||
if tc.proto == proxy.ProtoTLS {
|
conn net.Conn
|
||||||
conn = testTLSConn{
|
qconn quic.Connection
|
||||||
|
httpReq *http.Request
|
||||||
|
)
|
||||||
|
|
||||||
|
switch tc.proto {
|
||||||
|
case proxy.ProtoHTTPS:
|
||||||
|
u := &url.URL{
|
||||||
|
Path: "/dns-query",
|
||||||
|
}
|
||||||
|
|
||||||
|
connState := &tls.ConnectionState{
|
||||||
|
ServerName: tc.cliSrvName,
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq = &http.Request{
|
||||||
|
URL: u,
|
||||||
|
TLS: connState,
|
||||||
|
}
|
||||||
|
case proxy.ProtoQUIC:
|
||||||
|
qconn = testQUICConnection{
|
||||||
serverName: tc.cliSrvName,
|
serverName: tc.cliSrvName,
|
||||||
}
|
}
|
||||||
}
|
case proxy.ProtoTLS:
|
||||||
|
conn = testTLSConn{
|
||||||
var qconn quic.Connection
|
|
||||||
if tc.proto == proxy.ProtoQUIC {
|
|
||||||
qconn = testQUICConnection{
|
|
||||||
serverName: tc.cliSrvName,
|
serverName: tc.cliSrvName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,6 +222,7 @@ func TestServer_clientIDFromDNSContext(t *testing.T) {
|
||||||
pctx := &proxy.DNSContext{
|
pctx := &proxy.DNSContext{
|
||||||
Proto: tc.proto,
|
Proto: tc.proto,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
|
HTTPRequest: httpReq,
|
||||||
QUICConnection: qconn,
|
QUICConnection: qconn,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,56 +238,76 @@ func TestClientIDFromDNSContextHTTPS(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
path string
|
path string
|
||||||
|
cliSrvName string
|
||||||
wantClientID string
|
wantClientID string
|
||||||
wantErrMsg string
|
wantErrMsg string
|
||||||
}{{
|
}{{
|
||||||
name: "no_clientid",
|
name: "no_clientid",
|
||||||
path: "/dns-query",
|
path: "/dns-query",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "no_clientid_slash",
|
name: "no_clientid_slash",
|
||||||
path: "/dns-query/",
|
path: "/dns-query/",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid",
|
name: "clientid",
|
||||||
path: "/dns-query/cli",
|
path: "/dns-query/cli",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid_slash",
|
name: "clientid_slash",
|
||||||
path: "/dns-query/cli/",
|
path: "/dns-query/cli/",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "cli",
|
wantClientID: "cli",
|
||||||
wantErrMsg: "",
|
wantErrMsg: "",
|
||||||
}, {
|
}, {
|
||||||
name: "clientid_case",
|
name: "clientid_case",
|
||||||
path: "/dns-query/InSeNsItIvE",
|
path: "/dns-query/InSeNsItIvE",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "insensitive",
|
wantClientID: "insensitive",
|
||||||
wantErrMsg: ``,
|
wantErrMsg: ``,
|
||||||
}, {
|
}, {
|
||||||
name: "bad_url",
|
name: "bad_url",
|
||||||
path: "/foo",
|
path: "/foo",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid path "/foo"`,
|
wantErrMsg: `clientid check: invalid path "/foo"`,
|
||||||
}, {
|
}, {
|
||||||
name: "extra",
|
name: "extra",
|
||||||
path: "/dns-query/cli/foo",
|
path: "/dns-query/cli/foo",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
|
wantErrMsg: `clientid check: invalid path "/dns-query/cli/foo": extra parts`,
|
||||||
}, {
|
}, {
|
||||||
name: "invalid_clientid",
|
name: "invalid_clientid",
|
||||||
path: "/dns-query/!!!",
|
path: "/dns-query/!!!",
|
||||||
|
cliSrvName: "example.com",
|
||||||
wantClientID: "",
|
wantClientID: "",
|
||||||
wantErrMsg: `clientid check: invalid clientid "!!!": bad domain name label rune '!'`,
|
wantErrMsg: `clientid check: invalid clientid "!!!": bad domain name label rune '!'`,
|
||||||
|
}, {
|
||||||
|
name: "both_ids",
|
||||||
|
path: "/dns-query/right",
|
||||||
|
cliSrvName: "wrong.example.com",
|
||||||
|
wantClientID: "right",
|
||||||
|
wantErrMsg: "",
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
connState := &tls.ConnectionState{
|
||||||
|
ServerName: tc.cliSrvName,
|
||||||
|
}
|
||||||
|
|
||||||
r := &http.Request{
|
r := &http.Request{
|
||||||
URL: &url.URL{
|
URL: &url.URL{
|
||||||
Path: tc.path,
|
Path: tc.path,
|
||||||
},
|
},
|
||||||
|
TLS: connState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pctx := &proxy.DNSContext{
|
pctx := &proxy.DNSContext{
|
||||||
|
|
|
@ -81,6 +81,7 @@ type Server struct {
|
||||||
tableHostToIP hostToIPTable
|
tableHostToIP hostToIPTable
|
||||||
tableHostToIPLock sync.Mutex
|
tableHostToIPLock sync.Mutex
|
||||||
|
|
||||||
|
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
|
||||||
tableIPToHost *netutil.IPMap
|
tableIPToHost *netutil.IPMap
|
||||||
tableIPToHostLock sync.Mutex
|
tableIPToHostLock sync.Mutex
|
||||||
|
|
||||||
|
|
|
@ -453,13 +453,7 @@ func (d *DNSFilter) ApplyBlockedServices(setts *Settings, list []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesAvailableServices(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, serviceIDs)
|
||||||
err := json.NewEncoder(w).Encode(serviceIDs)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding available services: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -467,13 +461,7 @@ func (d *DNSFilter) handleBlockedServicesList(w http.ResponseWriter, r *http.Req
|
||||||
list := d.Config.BlockedServices
|
list := d.Config.BlockedServices
|
||||||
d.confLock.RUnlock()
|
d.confLock.RUnlock()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, list)
|
||||||
err := json.NewEncoder(w).Encode(list)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding services: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleBlockedServicesSet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -301,14 +301,7 @@ func (d *DNSFilter) handleFilteringRefresh(w http.ResponseWriter, r *http.Reques
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
|
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type filterJSON struct {
|
type filterJSON struct {
|
||||||
|
@ -361,17 +354,7 @@ func (d *DNSFilter) handleFilteringStatus(w http.ResponseWriter, r *http.Request
|
||||||
resp.UserRules = d.UserRules
|
resp.UserRules = d.UserRules
|
||||||
d.filtersMu.RUnlock()
|
d.filtersMu.RUnlock()
|
||||||
|
|
||||||
jsonVal, err := json.Marshal(resp)
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_, err = w.Write(jsonVal)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set filtering configuration
|
// Set filtering configuration
|
||||||
|
@ -473,11 +456,7 @@ func (d *DNSFilter) handleCheckHost(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
err = json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "encoding response: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterFilteringHandlers - register handlers
|
// RegisterFilteringHandlers - register handlers
|
||||||
|
|
|
@ -240,13 +240,7 @@ func (d *DNSFilter) handleRewriteList(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
d.confLock.Unlock()
|
d.confLock.Unlock()
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, arr)
|
||||||
err := json.NewEncoder(w).Encode(arr)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json.Encode: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleRewriteAdd(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -381,17 +380,13 @@ func (d *DNSFilter) handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
resp := &struct {
|
||||||
err := json.NewEncoder(w).Encode(&struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.SafeBrowsingEnabled,
|
Enabled: d.Config.SafeBrowsingEnabled,
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleParentalEnable(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -405,13 +400,11 @@ func (d *DNSFilter) handleParentalDisable(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleParentalStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
resp := &struct {
|
||||||
err := json.NewEncoder(w).Encode(&struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.ParentalEnabled,
|
Enabled: d.Config.ParentalEnabled,
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -146,21 +145,13 @@ func (d *DNSFilter) handleSafeSearchDisable(w http.ResponseWriter, r *http.Reque
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
func (d *DNSFilter) handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
resp := &struct {
|
||||||
err := json.NewEncoder(w).Encode(&struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
}{
|
}{
|
||||||
Enabled: d.Config.SafeSearchEnabled,
|
Enabled: d.Config.SafeSearchEnabled,
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(
|
|
||||||
r,
|
|
||||||
w,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
"Unable to write response json: %s",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
var safeSearchDomains = map[string]string{
|
var safeSearchDomains = map[string]string{
|
||||||
|
|
|
@ -12,16 +12,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
|
||||||
"github.com/AdguardTeam/golibs/testutil"
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
aghtest.DiscardLogOutput(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewSessionToken(t *testing.T) {
|
func TestNewSessionToken(t *testing.T) {
|
||||||
// Successful case.
|
// Successful case.
|
||||||
token, err := newSessionToken()
|
token, err := newSessionToken()
|
||||||
|
|
|
@ -119,6 +119,8 @@ type clientsContainer struct {
|
||||||
idIndex map[string]*Client // ID -> client
|
idIndex map[string]*Client // ID -> client
|
||||||
|
|
||||||
// ipToRC is the IP address to *RuntimeClient map.
|
// ipToRC is the IP address to *RuntimeClient map.
|
||||||
|
//
|
||||||
|
// TODO(e.burkov): Use map[netip.Addr]struct{} instead.
|
||||||
ipToRC *netutil.IPMap
|
ipToRC *netutil.IPMap
|
||||||
|
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
|
|
@ -2,6 +2,7 @@ package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
|
@ -287,10 +288,10 @@ func TestClientsAddExisting(t *testing.T) {
|
||||||
DBFilePath: "leases.db",
|
DBFilePath: "leases.db",
|
||||||
Conf4: dhcpd.V4ServerConf{
|
Conf4: dhcpd.V4ServerConf{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
GatewayIP: net.IP{1, 2, 3, 1},
|
GatewayIP: netip.MustParseAddr("1.2.3.1"),
|
||||||
SubnetMask: net.IP{255, 255, 255, 0},
|
SubnetMask: netip.MustParseAddr("255.255.255.0"),
|
||||||
RangeStart: net.IP{1, 2, 3, 2},
|
RangeStart: netip.MustParseAddr("1.2.3.2"),
|
||||||
RangeEnd: net.IP{1, 2, 3, 10},
|
RangeEnd: netip.MustParseAddr("1.2.3.10"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ package home
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
|
@ -85,19 +85,28 @@ type configuration struct {
|
||||||
// It's reset after config is parsed
|
// It's reset after config is parsed
|
||||||
fileData []byte
|
fileData []byte
|
||||||
|
|
||||||
BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
|
// BindHost is the address for the web interface server to listen on.
|
||||||
BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
|
BindHost netip.Addr `yaml:"bind_host"`
|
||||||
BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
|
// BindPort is the port for the web interface server to listen on.
|
||||||
Users []webUser `yaml:"users"` // Users that can access HTTP server
|
BindPort int `yaml:"bind_port"`
|
||||||
|
// BetaBindPort is the port for the new client's web interface server to
|
||||||
|
// listen on.
|
||||||
|
BetaBindPort int `yaml:"beta_bind_port"`
|
||||||
|
|
||||||
|
// Users are the clients capable for accessing the web interface.
|
||||||
|
Users []webUser `yaml:"users"`
|
||||||
// AuthAttempts is the maximum number of failed login attempts a user
|
// AuthAttempts is the maximum number of failed login attempts a user
|
||||||
// can do before being blocked.
|
// can do before being blocked.
|
||||||
AuthAttempts uint `yaml:"auth_attempts"`
|
AuthAttempts uint `yaml:"auth_attempts"`
|
||||||
// AuthBlockMin is the duration, in minutes, of the block of new login
|
// AuthBlockMin is the duration, in minutes, of the block of new login
|
||||||
// attempts after AuthAttempts unsuccessful login attempts.
|
// attempts after AuthAttempts unsuccessful login attempts.
|
||||||
AuthBlockMin uint `yaml:"block_auth_min"`
|
AuthBlockMin uint `yaml:"block_auth_min"`
|
||||||
ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
|
// ProxyURL is the address of proxy server for the internal HTTP client.
|
||||||
Language string `yaml:"language"` // two-letter ISO 639-1 language code
|
ProxyURL string `yaml:"http_proxy"`
|
||||||
DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
|
// Language is a two-letter ISO 639-1 language code.
|
||||||
|
Language string `yaml:"language"`
|
||||||
|
// DebugPProf defines if the profiling HTTP handler will listen on :6060.
|
||||||
|
DebugPProf bool `yaml:"debug_pprof"`
|
||||||
|
|
||||||
// TTL for a web session (in hours)
|
// TTL for a web session (in hours)
|
||||||
// An active session is automatically refreshed once a day.
|
// An active session is automatically refreshed once a day.
|
||||||
|
@ -112,7 +121,7 @@ type configuration struct {
|
||||||
//
|
//
|
||||||
// TODO(e.burkov): Move all the filtering configuration fields into the
|
// TODO(e.burkov): Move all the filtering configuration fields into the
|
||||||
// only configuration subsection covering the changes with a single
|
// only configuration subsection covering the changes with a single
|
||||||
// migration.
|
// migration. Also keep the blocked services in mind.
|
||||||
Filters []filtering.FilterYAML `yaml:"filters"`
|
Filters []filtering.FilterYAML `yaml:"filters"`
|
||||||
WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"`
|
WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"`
|
||||||
UserRules []string `yaml:"user_rules"`
|
UserRules []string `yaml:"user_rules"`
|
||||||
|
@ -135,18 +144,26 @@ type configuration struct {
|
||||||
|
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// field ordering is important -- yaml fields will mirror ordering from here
|
||||||
type dnsConfig struct {
|
type dnsConfig struct {
|
||||||
BindHosts []net.IP `yaml:"bind_hosts"`
|
BindHosts []netip.Addr `yaml:"bind_hosts"`
|
||||||
Port int `yaml:"port"`
|
Port int `yaml:"port"`
|
||||||
|
|
||||||
// time interval for statistics (in days)
|
// StatsInterval is the time interval for flushing statistics to the disk in
|
||||||
|
// days.
|
||||||
StatsInterval uint32 `yaml:"statistics_interval"`
|
StatsInterval uint32 `yaml:"statistics_interval"`
|
||||||
|
|
||||||
QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
|
// QueryLogEnabled defines if the query log is enabled.
|
||||||
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file
|
QueryLogEnabled bool `yaml:"querylog_enabled"`
|
||||||
|
// QueryLogFileEnabled defines, if the query log is written to the file.
|
||||||
|
QueryLogFileEnabled bool `yaml:"querylog_file_enabled"`
|
||||||
// QueryLogInterval is the interval for query log's files rotation.
|
// QueryLogInterval is the interval for query log's files rotation.
|
||||||
QueryLogInterval timeutil.Duration `yaml:"querylog_interval"`
|
QueryLogInterval timeutil.Duration `yaml:"querylog_interval"`
|
||||||
QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
|
// QueryLogMemSize is the number of entries kept in memory before they are
|
||||||
AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
|
// flushed to disk.
|
||||||
|
QueryLogMemSize uint32 `yaml:"querylog_size_memory"`
|
||||||
|
|
||||||
|
// AnonymizeClientIP defines if clients' IP addresses should be anonymized
|
||||||
|
// in query log and statistics.
|
||||||
|
AnonymizeClientIP bool `yaml:"anonymize_client_ip"`
|
||||||
|
|
||||||
dnsforward.FilteringConfig `yaml:",inline"`
|
dnsforward.FilteringConfig `yaml:",inline"`
|
||||||
|
|
||||||
|
@ -211,12 +228,12 @@ type tlsConfigSettings struct {
|
||||||
var config = &configuration{
|
var config = &configuration{
|
||||||
BindPort: 3000,
|
BindPort: 3000,
|
||||||
BetaBindPort: 0,
|
BetaBindPort: 0,
|
||||||
BindHost: net.IP{0, 0, 0, 0},
|
BindHost: netip.IPv4Unspecified(),
|
||||||
AuthAttempts: 5,
|
AuthAttempts: 5,
|
||||||
AuthBlockMin: 15,
|
AuthBlockMin: 15,
|
||||||
WebSessionTTLHours: 30 * 24,
|
WebSessionTTLHours: 30 * 24,
|
||||||
DNS: dnsConfig{
|
DNS: dnsConfig{
|
||||||
BindHosts: []net.IP{{0, 0, 0, 0}},
|
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||||
Port: defaultPortDNS,
|
Port: defaultPortDNS,
|
||||||
StatsInterval: 1,
|
StatsInterval: 1,
|
||||||
QueryLogEnabled: true,
|
QueryLogEnabled: true,
|
||||||
|
@ -236,6 +253,7 @@ var config = &configuration{
|
||||||
},
|
},
|
||||||
|
|
||||||
TrustedProxies: []string{"127.0.0.0/8", "::1/128"},
|
TrustedProxies: []string{"127.0.0.0/8", "::1/128"},
|
||||||
|
CacheSize: 4 * 1024 * 1024,
|
||||||
|
|
||||||
// set default maximum concurrent queries to 300
|
// set default maximum concurrent queries to 300
|
||||||
// we introduced a default limit due to this:
|
// we introduced a default limit due to this:
|
||||||
|
|
|
@ -2,8 +2,8 @@ package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -20,11 +20,11 @@ import (
|
||||||
|
|
||||||
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
|
||||||
// addresses to a slice of strings.
|
// addresses to a slice of strings.
|
||||||
func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
|
func appendDNSAddrs(dst []string, addrs ...netip.Addr) (res []string) {
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
var hostport string
|
var hostport string
|
||||||
if config.DNS.Port != defaultPortDNS {
|
if config.DNS.Port != defaultPortDNS {
|
||||||
hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port)
|
hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
|
||||||
} else {
|
} else {
|
||||||
hostport = addr.String()
|
hostport = addr.String()
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ func appendDNSAddrs(dst []string, addrs ...net.IP) (res []string) {
|
||||||
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
|
// appendDNSAddrsWithIfaces formats and appends all DNS addresses from src to
|
||||||
// dst. It also adds the IP addresses of all network interfaces if src contains
|
// dst. It also adds the IP addresses of all network interfaces if src contains
|
||||||
// an unspecified IP address.
|
// an unspecified IP address.
|
||||||
func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err error) {
|
func appendDNSAddrsWithIfaces(dst []string, src []netip.Addr) (res []string, err error) {
|
||||||
ifacesAdded := false
|
ifacesAdded := false
|
||||||
for _, h := range src {
|
for _, h := range src {
|
||||||
if !h.IsUnspecified() {
|
if !h.IsUnspecified() {
|
||||||
|
@ -71,7 +71,9 @@ func appendDNSAddrsWithIfaces(dst []string, src []net.IP) (res []string, err err
|
||||||
// on, including the addresses on all interfaces in cases of unspecified IPs.
|
// on, including the addresses on all interfaces in cases of unspecified IPs.
|
||||||
func collectDNSAddresses() (addrs []string, err error) {
|
func collectDNSAddresses() (addrs []string, err error) {
|
||||||
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
|
if hosts := config.DNS.BindHosts; len(hosts) == 0 {
|
||||||
addrs = appendDNSAddrs(addrs, net.IP{127, 0, 0, 1})
|
addr := aghnet.IPv4Localhost()
|
||||||
|
|
||||||
|
addrs = appendDNSAddrs(addrs, addr)
|
||||||
} else {
|
} else {
|
||||||
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -320,6 +322,28 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serveHTTP3 bool
|
||||||
|
var portHTTPS int
|
||||||
|
func() {
|
||||||
|
config.RLock()
|
||||||
|
defer config.RUnlock()
|
||||||
|
|
||||||
|
serveHTTP3, portHTTPS = config.DNS.ServeHTTP3, config.TLS.PortHTTPS
|
||||||
|
}()
|
||||||
|
|
||||||
|
respHdr := w.Header()
|
||||||
|
|
||||||
|
// Let the browser know that server supports HTTP/3.
|
||||||
|
//
|
||||||
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Alt-Svc.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider adding a configurable max-age. Currently, the
|
||||||
|
// default is 24 hours.
|
||||||
|
if serveHTTP3 {
|
||||||
|
altSvc := fmt.Sprintf(`h3=":%d"`, portHTTPS)
|
||||||
|
respHdr.Set(aghhttp.HdrNameAltSvc, altSvc)
|
||||||
|
}
|
||||||
|
|
||||||
if r.TLS == nil && web.forceHTTPS {
|
if r.TLS == nil && web.forceHTTPS {
|
||||||
hostPort := host
|
hostPort := host
|
||||||
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
|
if port := web.conf.PortHTTPS; port != defaultPortHTTPS {
|
||||||
|
@ -346,8 +370,9 @@ func handleHTTPSRedirect(w http.ResponseWriter, r *http.Request) (ok bool) {
|
||||||
Scheme: aghhttp.SchemeHTTP,
|
Scheme: aghhttp.SchemeHTTP,
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
}
|
}
|
||||||
w.Header().Set("Access-Control-Allow-Origin", originURL.String())
|
|
||||||
w.Header().Set("Vary", "Origin")
|
respHdr.Set(aghhttp.HdrNameAccessControlAllowOrigin, originURL.String())
|
||||||
|
respHdr.Set(aghhttp.HdrNameVary, aghhttp.HdrNameOrigin)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -64,9 +64,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkConfReqEnt struct {
|
type checkConfReqEnt struct {
|
||||||
IP net.IP `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Autofix bool `json:"autofix"`
|
Autofix bool `json:"autofix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type checkConfReq struct {
|
type checkConfReq struct {
|
||||||
|
@ -117,7 +117,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
|
||||||
// unbound after install.
|
// unbound after install.
|
||||||
}
|
}
|
||||||
|
|
||||||
return aghnet.CheckPort("tcp", req.Web.IP, portInt)
|
return aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(portInt)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateDNS returns error if the DNS part of the initial configuration can't
|
// validateDNS returns error if the DNS part of the initial configuration can't
|
||||||
|
@ -142,13 +142,13 @@ func (req *checkConfReq) validateDNS(
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("tcp", req.DNS.IP, port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
if !aghnet.IsAddrInUse(err) {
|
if !aghnet.IsAddrInUse(err) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ func (req *checkConfReq) validateDNS(
|
||||||
log.Error("disabling DNSStubListener: %s", err)
|
log.Error("disabling DNSStubListener: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(port)))
|
||||||
canAutofix = false
|
canAutofix = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,7 +196,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
|
||||||
// handleStaticIP - handles static IP request
|
// handleStaticIP - handles static IP request
|
||||||
// It either checks if we have a static IP
|
// It either checks if we have a static IP
|
||||||
// Or if set=true, it tries to set it
|
// Or if set=true, it tries to set it
|
||||||
func handleStaticIP(ip net.IP, set bool) staticIPJSON {
|
func handleStaticIP(ip netip.Addr, set bool) staticIPJSON {
|
||||||
resp := staticIPJSON{}
|
resp := staticIPJSON{}
|
||||||
|
|
||||||
interfaceName := aghnet.InterfaceByIP(ip)
|
interfaceName := aghnet.InterfaceByIP(ip)
|
||||||
|
@ -304,8 +304,8 @@ func disableDNSStubListener() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type applyConfigReqEnt struct {
|
type applyConfigReqEnt struct {
|
||||||
IP net.IP `json:"ip"`
|
IP netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type applyConfigReq struct {
|
type applyConfigReq struct {
|
||||||
|
@ -397,14 +397,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("udp", req.DNS.IP, req.DNS.Port)
|
err = aghnet.CheckPort("udp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = aghnet.CheckPort("tcp", req.DNS.IP, req.DNS.Port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.DNS.IP, uint16(req.DNS.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
|
||||||
|
@ -417,14 +417,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
Context.firstRun = false
|
Context.firstRun = false
|
||||||
config.BindHost = req.Web.IP
|
config.BindHost = req.Web.IP
|
||||||
config.BindPort = req.Web.Port
|
config.BindPort = req.Web.Port
|
||||||
config.DNS.BindHosts = []net.IP{req.DNS.IP}
|
config.DNS.BindHosts = []netip.Addr{req.DNS.IP}
|
||||||
config.DNS.Port = req.DNS.Port
|
config.DNS.Port = req.DNS.Port
|
||||||
|
|
||||||
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the
|
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the
|
||||||
// moment we'll allow setting up TLS in the initial configuration or the
|
// moment we'll allow setting up TLS in the initial configuration or the
|
||||||
// configuration itself will use HTTPS protocol, because the underlying
|
// configuration itself will use HTTPS protocol, because the underlying
|
||||||
// functions potentially restart the HTTPS server.
|
// functions potentially restart the HTTPS server.
|
||||||
err = StartMods()
|
err = startMods()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Context.firstRun = true
|
Context.firstRun = true
|
||||||
copyInstallSettings(config, curConfig)
|
copyInstallSettings(config, curConfig)
|
||||||
|
@ -490,9 +490,9 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
|
||||||
return nil, false, errors.Error("ports cannot be 0")
|
return nil, false, errors.Error("ports cannot be 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
restartHTTP = !config.BindHost.Equal(req.Web.IP) || config.BindPort != req.Web.Port
|
restartHTTP = config.BindHost != req.Web.IP || config.BindPort != req.Web.Port
|
||||||
if restartHTTP {
|
if restartHTTP {
|
||||||
err = aghnet.CheckPort("tcp", req.Web.IP, req.Web.Port)
|
err = aghnet.CheckPort("tcp", netip.AddrPortFrom(req.Web.IP, uint16(req.Web.Port)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, fmt.Errorf(
|
return nil, false, fmt.Errorf(
|
||||||
"checking address %s:%d: %w",
|
"checking address %s:%d: %w",
|
||||||
|
@ -518,9 +518,9 @@ func (web *Web) registerInstallHandlers() {
|
||||||
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
||||||
// functionality will appear in default checkConfigReqEnt.
|
// functionality will appear in default checkConfigReqEnt.
|
||||||
type checkConfigReqEntBeta struct {
|
type checkConfigReqEntBeta struct {
|
||||||
IP []net.IP `json:"ip"`
|
IP []netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Autofix bool `json:"autofix"`
|
Autofix bool `json:"autofix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkConfigReqBeta is a struct representing new client's config check request
|
// checkConfigReqBeta is a struct representing new client's config check request
|
||||||
|
@ -590,8 +590,8 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
|
||||||
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
// TODO(e.burkov): This should removed with the API v1 when the appropriate
|
||||||
// functionality will appear in default applyConfigReqEnt.
|
// functionality will appear in default applyConfigReqEnt.
|
||||||
type applyConfigReqEntBeta struct {
|
type applyConfigReqEntBeta struct {
|
||||||
IP []net.IP `json:"ip"`
|
IP []netip.Addr `json:"ip"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyConfigReqBeta is a struct representing new client's config setting
|
// applyConfigReqBeta is a struct representing new client's config setting
|
||||||
|
|
|
@ -3,6 +3,7 @@ package home
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -164,33 +165,27 @@ func onDNSRequest(pctx *proxy.DNSContext) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipsToTCPAddrs(ips []net.IP, port int) (tcpAddrs []*net.TCPAddr) {
|
func ipsToTCPAddrs(ips []netip.Addr, port int) (tcpAddrs []*net.TCPAddr) {
|
||||||
if ips == nil {
|
if ips == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpAddrs = make([]*net.TCPAddr, len(ips))
|
tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
|
||||||
for i, ip := range ips {
|
for _, ip := range ips {
|
||||||
tcpAddrs[i] = &net.TCPAddr{
|
tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
|
||||||
IP: ip,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tcpAddrs
|
return tcpAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ipsToUDPAddrs(ips []net.IP, port int) (udpAddrs []*net.UDPAddr) {
|
func ipsToUDPAddrs(ips []netip.Addr, port int) (udpAddrs []*net.UDPAddr) {
|
||||||
if ips == nil {
|
if ips == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
udpAddrs = make([]*net.UDPAddr, len(ips))
|
udpAddrs = make([]*net.UDPAddr, 0, len(ips))
|
||||||
for i, ip := range ips {
|
for _, ip := range ips {
|
||||||
udpAddrs[i] = &net.UDPAddr{
|
udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
|
||||||
IP: ip,
|
|
||||||
Port: port,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return udpAddrs
|
return udpAddrs
|
||||||
|
@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||||
dnsConf := config.DNS
|
dnsConf := config.DNS
|
||||||
hosts := dnsConf.BindHosts
|
hosts := dnsConf.BindHosts
|
||||||
if len(hosts) == 0 {
|
if len(hosts) == 0 {
|
||||||
hosts = []net.IP{{127, 0, 0, 1}}
|
hosts = []netip.Addr{aghnet.IPv4Localhost()}
|
||||||
}
|
}
|
||||||
|
|
||||||
newConf = dnsforward.ServerConfig{
|
newConf = dnsforward.ServerConfig{
|
||||||
|
@ -257,7 +252,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
|
||||||
return newConf, nil
|
return newConf, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDNSCrypt(hosts []net.IP, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
|
func newDNSCrypt(hosts []netip.Addr, tlsConf tlsConfigSettings) (dnscc dnsforward.DNSCryptConfig, err error) {
|
||||||
if tlsConf.DNSCryptConfigFile == "" {
|
if tlsConf.DNSCryptConfigFile == "" {
|
||||||
return dnscc, errors.Error("no dnscrypt_config_file")
|
return dnscc, errors.Error("no dnscrypt_config_file")
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
@ -58,7 +59,7 @@ type homeContext struct {
|
||||||
auth *Auth // HTTP authentication module
|
auth *Auth // HTTP authentication module
|
||||||
filters *filtering.DNSFilter // DNS filtering module
|
filters *filtering.DNSFilter // DNS filtering module
|
||||||
web *Web // Web (HTTP, HTTPS) module
|
web *Web // Web (HTTP, HTTPS) module
|
||||||
tls *TLSMod // TLS module
|
tls *tlsManager // TLS module
|
||||||
// etcHosts is an IP-hostname pairs set taken from system configuration
|
// etcHosts is an IP-hostname pairs set taken from system configuration
|
||||||
// (e.g. /etc/hosts) files.
|
// (e.g. /etc/hosts) files.
|
||||||
etcHosts *aghnet.HostsContainer
|
etcHosts *aghnet.HostsContainer
|
||||||
|
@ -97,9 +98,15 @@ var Context homeContext
|
||||||
|
|
||||||
// Main is the entry point
|
// Main is the entry point
|
||||||
func Main(clientBuildFS fs.FS) {
|
func Main(clientBuildFS fs.FS) {
|
||||||
// config can be specified, which reads options from there, but other command line flags have to override config values
|
initCmdLineOpts()
|
||||||
// therefore, we must do it manually instead of using a lib
|
|
||||||
args := loadOptions()
|
// The configuration file path can be overridden, but other command-line
|
||||||
|
// options have to override config values. Therefore, do it manually
|
||||||
|
// instead of using package flag.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): The comment above is most likely false. Replace with
|
||||||
|
// package flag.
|
||||||
|
opts := loadCmdLineOpts()
|
||||||
|
|
||||||
Context.appSignalChannel = make(chan os.Signal)
|
Context.appSignalChannel = make(chan os.Signal)
|
||||||
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
signal.Notify(Context.appSignalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
|
||||||
|
@ -110,7 +117,7 @@ func Main(clientBuildFS fs.FS) {
|
||||||
switch sig {
|
switch sig {
|
||||||
case syscall.SIGHUP:
|
case syscall.SIGHUP:
|
||||||
Context.clients.Reload()
|
Context.clients.Reload()
|
||||||
Context.tls.Reload()
|
Context.tls.reload()
|
||||||
|
|
||||||
default:
|
default:
|
||||||
cleanup(context.Background())
|
cleanup(context.Background())
|
||||||
|
@ -120,26 +127,18 @@ func Main(clientBuildFS fs.FS) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if args.serviceControlAction != "" {
|
if opts.serviceControlAction != "" {
|
||||||
handleServiceControlAction(args, clientBuildFS)
|
handleServiceControlAction(opts, clientBuildFS)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// run the protection
|
// run the protection
|
||||||
run(args, clientBuildFS)
|
run(opts, clientBuildFS)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupContext(args options) {
|
func setupContext(opts options) {
|
||||||
Context.runningAsService = args.runningAsService
|
setupContextFlags(opts)
|
||||||
Context.disableUpdate = args.disableUpdate ||
|
|
||||||
version.Channel() == version.ChannelDevelopment
|
|
||||||
|
|
||||||
Context.firstRun = detectFirstRun()
|
|
||||||
if Context.firstRun {
|
|
||||||
log.Info("This is the first time AdGuard Home is launched")
|
|
||||||
checkPermissions()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch version.Channel() {
|
switch version.Channel() {
|
||||||
case version.ChannelEdge, version.ChannelDevelopment:
|
case version.ChannelEdge, version.ChannelDevelopment:
|
||||||
|
@ -148,7 +147,7 @@ func setupContext(args options) {
|
||||||
// Go on.
|
// Go on.
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.tlsRoots = LoadSystemRootCAs()
|
Context.tlsRoots = aghtls.SystemRootCAs()
|
||||||
Context.transport = &http.Transport{
|
Context.transport = &http.Transport{
|
||||||
DialContext: customDialContext,
|
DialContext: customDialContext,
|
||||||
Proxy: getHTTPProxy,
|
Proxy: getHTTPProxy,
|
||||||
|
@ -174,13 +173,13 @@ func setupContext(args options) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.checkConfig {
|
if opts.checkConfig {
|
||||||
log.Info("configuration file is ok")
|
log.Info("configuration file is ok")
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !args.noEtcHosts && config.Clients.Sources.HostsFile {
|
if !opts.noEtcHosts && config.Clients.Sources.HostsFile {
|
||||||
err = setupHostsContainer()
|
err = setupHostsContainer()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
}
|
}
|
||||||
|
@ -189,6 +188,24 @@ func setupContext(args options) {
|
||||||
Context.mux = http.NewServeMux()
|
Context.mux = http.NewServeMux()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setupContextFlags sets global flags and prints their status to the log.
|
||||||
|
func setupContextFlags(opts options) {
|
||||||
|
Context.firstRun = detectFirstRun()
|
||||||
|
if Context.firstRun {
|
||||||
|
log.Info("This is the first time AdGuard Home is launched")
|
||||||
|
checkPermissions()
|
||||||
|
}
|
||||||
|
|
||||||
|
Context.runningAsService = opts.runningAsService
|
||||||
|
// Don't print the runningAsService flag, since that has already been done
|
||||||
|
// in [run].
|
||||||
|
|
||||||
|
Context.disableUpdate = opts.disableUpdate || version.Channel() == version.ChannelDevelopment
|
||||||
|
if Context.disableUpdate {
|
||||||
|
log.Info("AdGuard Home updates are disabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// logIfUnsupported logs a formatted warning if the error is one of the
|
// logIfUnsupported logs a formatted warning if the error is one of the
|
||||||
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
// unsupported errors and returns nil. If err is nil, logIfUnsupported returns
|
||||||
// nil. Otherwise, it returns err.
|
// nil. Otherwise, it returns err.
|
||||||
|
@ -270,7 +287,7 @@ func setupHostsContainer() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupConfig(args options) (err error) {
|
func setupConfig(opts options) (err error) {
|
||||||
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
|
config.DNS.DnsfilterConf.EtcHosts = Context.etcHosts
|
||||||
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
|
config.DNS.DnsfilterConf.ConfigModified = onConfigModified
|
||||||
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
|
config.DNS.DnsfilterConf.HTTPRegister = httpRegister
|
||||||
|
@ -312,9 +329,9 @@ func setupConfig(args options) (err error) {
|
||||||
|
|
||||||
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
|
Context.clients.Init(config.Clients.Persistent, Context.dhcpServer, Context.etcHosts, arpdb)
|
||||||
|
|
||||||
if args.bindPort != 0 {
|
if opts.bindPort != 0 {
|
||||||
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
tcpPorts := aghalg.UniqChecker[tcpPort]{}
|
||||||
addPorts(tcpPorts, tcpPort(args.bindPort), tcpPort(config.BetaBindPort))
|
addPorts(tcpPorts, tcpPort(opts.bindPort), tcpPort(config.BetaBindPort))
|
||||||
|
|
||||||
udpPorts := aghalg.UniqChecker[udpPort]{}
|
udpPorts := aghalg.UniqChecker[udpPort]{}
|
||||||
addPorts(udpPorts, udpPort(config.DNS.Port))
|
addPorts(udpPorts, udpPort(config.DNS.Port))
|
||||||
|
@ -336,23 +353,23 @@ func setupConfig(args options) (err error) {
|
||||||
return fmt.Errorf("validating udp ports: %w", err)
|
return fmt.Errorf("validating udp ports: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.BindPort = args.bindPort
|
config.BindPort = opts.bindPort
|
||||||
}
|
}
|
||||||
|
|
||||||
// override bind host/port from the console
|
// override bind host/port from the console
|
||||||
if args.bindHost != nil {
|
if opts.bindHost.IsValid() {
|
||||||
config.BindHost = args.bindHost
|
config.BindHost = opts.bindHost
|
||||||
}
|
}
|
||||||
if len(args.pidFile) != 0 && writePIDFile(args.pidFile) {
|
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
|
||||||
Context.pidFileName = args.pidFile
|
Context.pidFileName = opts.pidFile
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initWeb(args options, clientBuildFS fs.FS) (web *Web, err error) {
|
func initWeb(opts options, clientBuildFS fs.FS) (web *Web, err error) {
|
||||||
var clientFS, clientBetaFS fs.FS
|
var clientFS, clientBetaFS fs.FS
|
||||||
if args.localFrontend {
|
if opts.localFrontend {
|
||||||
log.Info("warning: using local frontend files")
|
log.Info("warning: using local frontend files")
|
||||||
|
|
||||||
clientFS = os.DirFS("build/static")
|
clientFS = os.DirFS("build/static")
|
||||||
|
@ -406,24 +423,24 @@ func fatalOnError(err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// run configures and starts AdGuard Home.
|
// run configures and starts AdGuard Home.
|
||||||
func run(args options, clientBuildFS fs.FS) {
|
func run(opts options, clientBuildFS fs.FS) {
|
||||||
// configure config filename
|
// configure config filename
|
||||||
initConfigFilename(args)
|
initConfigFilename(opts)
|
||||||
|
|
||||||
// configure working dir and config path
|
// configure working dir and config path
|
||||||
initWorkingDir(args)
|
initWorkingDir(opts)
|
||||||
|
|
||||||
// configure log level and output
|
// configure log level and output
|
||||||
configureLogger(args)
|
configureLogger(opts)
|
||||||
|
|
||||||
// Print the first message after logger is configured.
|
// Print the first message after logger is configured.
|
||||||
log.Info(version.Full())
|
log.Info(version.Full())
|
||||||
log.Debug("current working directory is %s", Context.workDir)
|
log.Debug("current working directory is %s", Context.workDir)
|
||||||
if args.runningAsService {
|
if opts.runningAsService {
|
||||||
log.Info("AdGuard Home is running as a service")
|
log.Info("AdGuard Home is running as a service")
|
||||||
}
|
}
|
||||||
|
|
||||||
setupContext(args)
|
setupContext(opts)
|
||||||
|
|
||||||
err := configureOS(config)
|
err := configureOS(config)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
@ -433,7 +450,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||||
// but also avoid relying on automatic Go init() function
|
// but also avoid relying on automatic Go init() function
|
||||||
filtering.InitModule()
|
filtering.InitModule()
|
||||||
|
|
||||||
err = setupConfig(args)
|
err = setupConfig(opts)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
|
@ -462,7 +479,7 @@ func run(args options, clientBuildFS fs.FS) {
|
||||||
}
|
}
|
||||||
|
|
||||||
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
sessFilename := filepath.Join(Context.getDataDir(), "sessions.db")
|
||||||
GLMode = args.glinetMode
|
GLMode = opts.glinetMode
|
||||||
var rateLimiter *authRateLimiter
|
var rateLimiter *authRateLimiter
|
||||||
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
if config.AuthAttempts > 0 && config.AuthBlockMin > 0 {
|
||||||
rateLimiter = newAuthRateLimiter(
|
rateLimiter = newAuthRateLimiter(
|
||||||
|
@ -484,19 +501,19 @@ func run(args options, clientBuildFS fs.FS) {
|
||||||
}
|
}
|
||||||
config.Users = nil
|
config.Users = nil
|
||||||
|
|
||||||
Context.tls = tlsCreate(config.TLS)
|
Context.tls, err = newTLSManager(config.TLS)
|
||||||
if Context.tls == nil {
|
if err != nil {
|
||||||
log.Fatalf("Can't initialize TLS module")
|
log.Fatalf("initializing tls: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.web, err = initWeb(args, clientBuildFS)
|
Context.web, err = initWeb(opts, clientBuildFS)
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
if !Context.firstRun {
|
if !Context.firstRun {
|
||||||
err = initDNSServer()
|
err = initDNSServer()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
Context.tls.Start()
|
Context.tls.start()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
serr := startDNSServer()
|
serr := startDNSServer()
|
||||||
|
@ -520,20 +537,22 @@ func run(args options, clientBuildFS fs.FS) {
|
||||||
select {}
|
select {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartMods initializes and starts the DNS server after installation.
|
// startMods initializes and starts the DNS server after installation.
|
||||||
func StartMods() error {
|
func startMods() error {
|
||||||
err := initDNSServer()
|
err := initDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.tls.Start()
|
Context.tls.start()
|
||||||
|
|
||||||
err = startDNSServer()
|
err = startDNSServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
closeDNSServer()
|
closeDNSServer()
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -546,7 +565,7 @@ func checkPermissions() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// We should check if AdGuard Home is able to bind to port 53
|
// We should check if AdGuard Home is able to bind to port 53
|
||||||
err := aghnet.CheckPort("tcp", net.IP{127, 0, 0, 1}, defaultPortDNS)
|
err := aghnet.CheckPort("tcp", netip.AddrPortFrom(aghnet.IPv4Localhost(), defaultPortDNS))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrPermission) {
|
if errors.Is(err, os.ErrPermission) {
|
||||||
log.Fatal(`Permission check failed.
|
log.Fatal(`Permission check failed.
|
||||||
|
@ -581,10 +600,10 @@ func writePIDFile(fn string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func initConfigFilename(args options) {
|
func initConfigFilename(opts options) {
|
||||||
// config file path can be overridden by command-line arguments:
|
// config file path can be overridden by command-line arguments:
|
||||||
if args.configFilename != "" {
|
if opts.confFilename != "" {
|
||||||
Context.configFilename = args.configFilename
|
Context.configFilename = opts.confFilename
|
||||||
} else {
|
} else {
|
||||||
// Default config file name
|
// Default config file name
|
||||||
Context.configFilename = "AdGuardHome.yaml"
|
Context.configFilename = "AdGuardHome.yaml"
|
||||||
|
@ -593,15 +612,15 @@ func initConfigFilename(args options) {
|
||||||
|
|
||||||
// initWorkingDir initializes the workDir
|
// initWorkingDir initializes the workDir
|
||||||
// if no command-line arguments specified, we use the directory where our binary file is located
|
// if no command-line arguments specified, we use the directory where our binary file is located
|
||||||
func initWorkingDir(args options) {
|
func initWorkingDir(opts options) {
|
||||||
execPath, err := os.Executable()
|
execPath, err := os.Executable()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.workDir != "" {
|
if opts.workDir != "" {
|
||||||
// If there is a custom config file, use it's directory as our working dir
|
// If there is a custom config file, use it's directory as our working dir
|
||||||
Context.workDir = args.workDir
|
Context.workDir = opts.workDir
|
||||||
} else {
|
} else {
|
||||||
Context.workDir = filepath.Dir(execPath)
|
Context.workDir = filepath.Dir(execPath)
|
||||||
}
|
}
|
||||||
|
@ -615,15 +634,15 @@ func initWorkingDir(args options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// configureLogger configures logger level and output
|
// configureLogger configures logger level and output
|
||||||
func configureLogger(args options) {
|
func configureLogger(opts options) {
|
||||||
ls := getLogSettings()
|
ls := getLogSettings()
|
||||||
|
|
||||||
// command-line arguments can override config settings
|
// command-line arguments can override config settings
|
||||||
if args.verbose || config.Verbose {
|
if opts.verbose || config.Verbose {
|
||||||
ls.Verbose = true
|
ls.Verbose = true
|
||||||
}
|
}
|
||||||
if args.logFile != "" {
|
if opts.logFile != "" {
|
||||||
ls.File = args.logFile
|
ls.File = opts.logFile
|
||||||
} else if config.File != "" {
|
} else if config.File != "" {
|
||||||
ls.File = config.File
|
ls.File = config.File
|
||||||
}
|
}
|
||||||
|
@ -644,7 +663,7 @@ func configureLogger(args options) {
|
||||||
// happen pretty quickly.
|
// happen pretty quickly.
|
||||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||||
|
|
||||||
if args.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
if opts.runningAsService && ls.File == "" && runtime.GOOS == "windows" {
|
||||||
// When running as a Windows service, use eventlog by default if nothing
|
// When running as a Windows service, use eventlog by default if nothing
|
||||||
// else is configured. Otherwise, we'll simply lose the log output.
|
// else is configured. Otherwise, we'll simply lose the log output.
|
||||||
ls.File = configSyslog
|
ls.File = configSyslog
|
||||||
|
@ -717,7 +736,6 @@ func cleanup(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if Context.tls != nil {
|
if Context.tls != nil {
|
||||||
Context.tls.Close()
|
|
||||||
Context.tls = nil
|
Context.tls = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -727,32 +745,37 @@ func cleanupAlways() {
|
||||||
if len(Context.pidFileName) != 0 {
|
if len(Context.pidFileName) != 0 {
|
||||||
_ = os.Remove(Context.pidFileName)
|
_ = os.Remove(Context.pidFileName)
|
||||||
}
|
}
|
||||||
log.Info("Stopped")
|
|
||||||
|
log.Info("stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func exitWithError() {
|
func exitWithError() {
|
||||||
os.Exit(64)
|
os.Exit(64)
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadOptions reads command line arguments and initializes configuration
|
// loadCmdLineOpts reads command line arguments and initializes configuration
|
||||||
func loadOptions() options {
|
// from them. If there is an error or an effect, loadCmdLineOpts processes them
|
||||||
o, f, err := parse(os.Args[0], os.Args[1:])
|
// and exits.
|
||||||
|
func loadCmdLineOpts() (opts options) {
|
||||||
|
opts, eff, err := parseCmdOpts(os.Args[0], os.Args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
_ = printHelp(os.Args[0])
|
printHelp(os.Args[0])
|
||||||
|
|
||||||
exitWithError()
|
exitWithError()
|
||||||
} else if f != nil {
|
}
|
||||||
err = f()
|
|
||||||
|
if eff != nil {
|
||||||
|
err = eff()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
exitWithError()
|
exitWithError()
|
||||||
} else {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return o
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
// printWebAddrs prints addresses built from proto, addr, and an appropriate
|
||||||
|
@ -901,6 +924,6 @@ func getTLSCiphers() (cipherIds []uint16, err error) {
|
||||||
return aghtls.SaferCipherSuites(), nil
|
return aghtls.SaferCipherSuites(), nil
|
||||||
} else {
|
} else {
|
||||||
log.Info("Overriding TLS Ciphers : %s", config.TLS.OverrideTLSCiphers)
|
log.Info("Overriding TLS Ciphers : %s", config.TLS.OverrideTLSCiphers)
|
||||||
return aghtls.ParseCipherIDs(config.TLS.OverrideTLSCiphers)
|
return aghtls.ParseCiphers(config.TLS.OverrideTLSCiphers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
package home
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
aghtest.DiscardLogOutput(m)
|
||||||
|
initCmdLineOpts()
|
||||||
|
}
|
|
@ -3,12 +3,11 @@ package home
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AdguardTeam/golibs/netutil"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"howett.net/plist"
|
"howett.net/plist"
|
||||||
|
@ -28,12 +27,12 @@ func setupDNSIPs(t testing.TB) {
|
||||||
|
|
||||||
config = &configuration{
|
config = &configuration{
|
||||||
DNS: dnsConfig{
|
DNS: dnsConfig{
|
||||||
BindHosts: []net.IP{netutil.IPv4Zero()},
|
BindHosts: []netip.Addr{netip.IPv4Unspecified()},
|
||||||
Port: defaultPortDNS,
|
Port: defaultPortDNS,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.tls = &TLSMod{}
|
Context.tls = &tlsManager{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleMobileConfigDoH(t *testing.T) {
|
func TestHandleMobileConfigDoH(t *testing.T) {
|
||||||
|
@ -66,7 +65,7 @@ func TestHandleMobileConfigDoH(t *testing.T) {
|
||||||
oldTLSConf := Context.tls
|
oldTLSConf := Context.tls
|
||||||
t.Cleanup(func() { Context.tls = oldTLSConf })
|
t.Cleanup(func() { Context.tls = oldTLSConf })
|
||||||
|
|
||||||
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
|
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
|
||||||
|
|
||||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
|
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/doh.mobileconfig", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
@ -138,7 +137,7 @@ func TestHandleMobileConfigDoT(t *testing.T) {
|
||||||
oldTLSConf := Context.tls
|
oldTLSConf := Context.tls
|
||||||
t.Cleanup(func() { Context.tls = oldTLSConf })
|
t.Cleanup(func() { Context.tls = oldTLSConf })
|
||||||
|
|
||||||
Context.tls = &TLSMod{conf: tlsConfigSettings{}}
|
Context.tls = &tlsManager{conf: tlsConfigSettings{}}
|
||||||
|
|
||||||
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
|
r, err := http.NewRequest(http.MethodGet, "https://example.com:12345/apple/dot.mobileconfig", nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -2,33 +2,63 @@ package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"github.com/AdguardTeam/golibs/stringutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// options passed from command-line arguments
|
// TODO(a.garipov): Replace with package flag.
|
||||||
type options struct {
|
|
||||||
verbose bool // is verbose logging enabled
|
|
||||||
configFilename string // path to the config file
|
|
||||||
workDir string // path to the working directory where we will store the filters data and the querylog
|
|
||||||
bindHost net.IP // host address to bind HTTP server on
|
|
||||||
bindPort int // port to serve HTTP pages on
|
|
||||||
logFile string // Path to the log file. If empty, write to stdout. If "syslog", writes to syslog
|
|
||||||
pidFile string // File name to save PID to
|
|
||||||
checkConfig bool // Check configuration and exit
|
|
||||||
disableUpdate bool // If set, don't check for updates
|
|
||||||
|
|
||||||
// service control action (see service.ControlAction array + "status" command)
|
// options represents the command-line options.
|
||||||
|
type options struct {
|
||||||
|
// confFilename is the path to the configuration file.
|
||||||
|
confFilename string
|
||||||
|
|
||||||
|
// workDir is the path to the working directory where AdGuard Home stores
|
||||||
|
// filter data, the query log, and other data.
|
||||||
|
workDir string
|
||||||
|
|
||||||
|
// logFile is the path to the log file. If empty, AdGuard Home writes to
|
||||||
|
// stdout; if "syslog", to syslog.
|
||||||
|
logFile string
|
||||||
|
|
||||||
|
// pidFile is the file name for the file to which the PID is saved.
|
||||||
|
pidFile string
|
||||||
|
|
||||||
|
// serviceControlAction is the service action to perform. See
|
||||||
|
// [service.ControlAction] and [handleServiceControlAction].
|
||||||
serviceControlAction string
|
serviceControlAction string
|
||||||
|
|
||||||
// runningAsService flag is set to true when options are passed from the service runner
|
// bindHost is the address on which to serve the HTTP UI.
|
||||||
|
bindHost netip.Addr
|
||||||
|
|
||||||
|
// bindPort is the port on which to serve the HTTP UI.
|
||||||
|
bindPort int
|
||||||
|
|
||||||
|
// checkConfig is true if the current invocation is only required to check
|
||||||
|
// the configuration file and exit.
|
||||||
|
checkConfig bool
|
||||||
|
|
||||||
|
// disableUpdate, if set, makes AdGuard Home not check for updates.
|
||||||
|
disableUpdate bool
|
||||||
|
|
||||||
|
// verbose shows if verbose logging is enabled.
|
||||||
|
verbose bool
|
||||||
|
|
||||||
|
// runningAsService flag is set to true when options are passed from the
|
||||||
|
// service runner
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Perhaps this could be determined by a non-empty
|
||||||
|
// serviceControlAction?
|
||||||
runningAsService bool
|
runningAsService bool
|
||||||
|
|
||||||
glinetMode bool // Activate GL-Inet compatibility mode
|
// glinetMode shows if the GL-Inet compatibility mode is enabled.
|
||||||
|
glinetMode bool
|
||||||
|
|
||||||
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
|
// noEtcHosts flag should be provided when /etc/hosts file shouldn't be
|
||||||
// used.
|
// used.
|
||||||
|
@ -39,88 +69,86 @@ type options struct {
|
||||||
localFrontend bool
|
localFrontend bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// functions used for their side-effects
|
// initCmdLineOpts completes initialization of the global command-line option
|
||||||
type effect func() error
|
// slice. It must only be called once.
|
||||||
|
func initCmdLineOpts() {
|
||||||
type arg struct {
|
// The --help option cannot be put directly into cmdLineOpts, because that
|
||||||
description string // a short, English description of the argument
|
// causes initialization cycle due to printHelp referencing cmdLineOpts.
|
||||||
longName string // the name of the argument used after '--'
|
cmdLineOpts = append(cmdLineOpts, cmdLineOpt{
|
||||||
shortName string // the name of the argument used after '-'
|
updateWithValue: nil,
|
||||||
|
updateNoValue: nil,
|
||||||
// only one of updateWithValue, updateNoValue, and effect should be present
|
effect: func(o options, exec string) (effect, error) {
|
||||||
|
return func() error { printHelp(exec); exitWithError(); return nil }, nil
|
||||||
updateWithValue func(o options, v string) (options, error) // the mutator for arguments with parameters
|
},
|
||||||
updateNoValue func(o options) (options, error) // the mutator for arguments without parameters
|
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||||
effect func(o options, exec string) (f effect, err error) // the side-effect closure generator
|
description: "Print this help.",
|
||||||
|
longName: "help",
|
||||||
serialize func(o options) []string // the re-serialization function back to arguments (return nil for omit)
|
shortName: "",
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// {type}SliceOrNil functions check their parameter of type {type}
|
// effect is the type for functions used for their side-effects.
|
||||||
// against its zero value and return nil if the parameter value is
|
type effect func() (err error)
|
||||||
// zero otherwise they return a string slice of the parameter
|
|
||||||
|
|
||||||
func ipSliceOrNil(ip net.IP) []string {
|
// cmdLineOpt contains the data for a single command-line option. Only one of
|
||||||
if ip == nil {
|
// updateWithValue, updateNoValue, and effect must be present.
|
||||||
return nil
|
type cmdLineOpt struct {
|
||||||
}
|
updateWithValue func(o options, v string) (updated options, err error)
|
||||||
|
updateNoValue func(o options) (updated options, err error)
|
||||||
|
effect func(o options, exec string) (eff effect, err error)
|
||||||
|
|
||||||
return []string{ip.String()}
|
// serialize is a function that encodes the option into a slice of
|
||||||
|
// command-line arguments, if necessary. If ok is false, this option should
|
||||||
|
// be skipped.
|
||||||
|
serialize func(o options) (val string, ok bool)
|
||||||
|
|
||||||
|
description string
|
||||||
|
longName string
|
||||||
|
shortName string
|
||||||
}
|
}
|
||||||
|
|
||||||
func stringSliceOrNil(s string) []string {
|
// cmdLineOpts are all command-line options of AdGuard Home.
|
||||||
if s == "" {
|
var cmdLineOpts = []cmdLineOpt{{
|
||||||
return nil
|
updateWithValue: func(o options, v string) (options, error) {
|
||||||
}
|
o.confFilename = v
|
||||||
|
return o, nil
|
||||||
|
},
|
||||||
|
updateNoValue: nil,
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) {
|
||||||
|
return o.confFilename, o.confFilename != ""
|
||||||
|
},
|
||||||
|
description: "Path to the config file.",
|
||||||
|
longName: "config",
|
||||||
|
shortName: "c",
|
||||||
|
}, {
|
||||||
|
updateWithValue: func(o options, v string) (options, error) { o.workDir = v; return o, nil },
|
||||||
|
updateNoValue: nil,
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return o.workDir, o.workDir != "" },
|
||||||
|
description: "Path to the working directory.",
|
||||||
|
longName: "work-dir",
|
||||||
|
shortName: "w",
|
||||||
|
}, {
|
||||||
|
updateWithValue: func(o options, v string) (oo options, err error) {
|
||||||
|
o.bindHost, err = netip.ParseAddr(v)
|
||||||
|
|
||||||
return []string{s}
|
return o, err
|
||||||
}
|
},
|
||||||
|
updateNoValue: nil,
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) {
|
||||||
|
if !o.bindHost.IsValid() {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
func intSliceOrNil(i int) []string {
|
return o.bindHost.String(), true
|
||||||
if i == 0 {
|
},
|
||||||
return nil
|
description: "Host address to bind HTTP server on.",
|
||||||
}
|
longName: "host",
|
||||||
|
shortName: "h",
|
||||||
return []string{strconv.Itoa(i)}
|
}, {
|
||||||
}
|
updateWithValue: func(o options, v string) (options, error) {
|
||||||
|
|
||||||
func boolSliceOrNil(b bool) []string {
|
|
||||||
if b {
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var args []arg
|
|
||||||
|
|
||||||
var configArg = arg{
|
|
||||||
"Path to the config file.",
|
|
||||||
"config", "c",
|
|
||||||
func(o options, v string) (options, error) { o.configFilename = v; return o, nil },
|
|
||||||
nil,
|
|
||||||
nil,
|
|
||||||
func(o options) []string { return stringSliceOrNil(o.configFilename) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var workDirArg = arg{
|
|
||||||
"Path to the working directory.",
|
|
||||||
"work-dir", "w",
|
|
||||||
func(o options, v string) (options, error) { o.workDir = v; return o, nil }, nil, nil,
|
|
||||||
func(o options) []string { return stringSliceOrNil(o.workDir) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostArg = arg{
|
|
||||||
"Host address to bind HTTP server on.",
|
|
||||||
"host", "h",
|
|
||||||
func(o options, v string) (options, error) { o.bindHost = net.ParseIP(v); return o, nil }, nil, nil,
|
|
||||||
func(o options) []string { return ipSliceOrNil(o.bindHost) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var portArg = arg{
|
|
||||||
"Port to serve HTTP pages on.",
|
|
||||||
"port", "p",
|
|
||||||
func(o options, v string) (options, error) {
|
|
||||||
var err error
|
var err error
|
||||||
var p int
|
var p int
|
||||||
minPort, maxPort := 0, 1<<16-1
|
minPort, maxPort := 0, 1<<16-1
|
||||||
|
@ -131,108 +159,81 @@ var portArg = arg{
|
||||||
} else {
|
} else {
|
||||||
o.bindPort = p
|
o.bindPort = p
|
||||||
}
|
}
|
||||||
return o, err
|
|
||||||
}, nil, nil,
|
|
||||||
func(o options) []string { return intSliceOrNil(o.bindPort) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceArg = arg{
|
return o, err
|
||||||
"Service control action: status, install, uninstall, start, stop, restart, reload (configuration).",
|
},
|
||||||
"service", "s",
|
updateNoValue: nil,
|
||||||
func(o options, v string) (options, error) {
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) {
|
||||||
|
if o.bindPort == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return strconv.Itoa(o.bindPort), true
|
||||||
|
},
|
||||||
|
description: "Port to serve HTTP pages on.",
|
||||||
|
longName: "port",
|
||||||
|
shortName: "p",
|
||||||
|
}, {
|
||||||
|
updateWithValue: func(o options, v string) (options, error) {
|
||||||
o.serviceControlAction = v
|
o.serviceControlAction = v
|
||||||
return o, nil
|
return o, nil
|
||||||
}, nil, nil,
|
},
|
||||||
func(o options) []string { return stringSliceOrNil(o.serviceControlAction) },
|
updateNoValue: nil,
|
||||||
}
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) {
|
||||||
var logfileArg = arg{
|
return o.serviceControlAction, o.serviceControlAction != ""
|
||||||
"Path to log file. If empty: write to stdout; if 'syslog': write to system log.",
|
},
|
||||||
"logfile", "l",
|
description: `Service control action: status, install (as a service), ` +
|
||||||
func(o options, v string) (options, error) { o.logFile = v; return o, nil }, nil, nil,
|
`uninstall (as a service), start, stop, restart, reload (configuration).`,
|
||||||
func(o options) []string { return stringSliceOrNil(o.logFile) },
|
longName: "service",
|
||||||
}
|
shortName: "s",
|
||||||
|
}, {
|
||||||
var pidfileArg = arg{
|
updateWithValue: func(o options, v string) (options, error) { o.logFile = v; return o, nil },
|
||||||
"Path to a file where PID is stored.",
|
updateNoValue: nil,
|
||||||
"pidfile", "",
|
effect: nil,
|
||||||
func(o options, v string) (options, error) { o.pidFile = v; return o, nil }, nil, nil,
|
serialize: func(o options) (val string, ok bool) { return o.logFile, o.logFile != "" },
|
||||||
func(o options) []string { return stringSliceOrNil(o.pidFile) },
|
description: `Path to log file. If empty, write to stdout; ` +
|
||||||
}
|
`if "syslog", write to system log.`,
|
||||||
|
longName: "logfile",
|
||||||
var checkConfigArg = arg{
|
shortName: "l",
|
||||||
"Check configuration and exit.",
|
}, {
|
||||||
"check-config", "",
|
updateWithValue: func(o options, v string) (options, error) { o.pidFile = v; return o, nil },
|
||||||
nil, func(o options) (options, error) { o.checkConfig = true; return o, nil }, nil,
|
updateNoValue: nil,
|
||||||
func(o options) []string { return boolSliceOrNil(o.checkConfig) },
|
effect: nil,
|
||||||
}
|
serialize: func(o options) (val string, ok bool) { return o.pidFile, o.pidFile != "" },
|
||||||
|
description: "Path to a file where PID is stored.",
|
||||||
var noCheckUpdateArg = arg{
|
longName: "pidfile",
|
||||||
"Don't check for updates.",
|
shortName: "",
|
||||||
"no-check-update", "",
|
}, {
|
||||||
nil, func(o options) (options, error) { o.disableUpdate = true; return o, nil }, nil,
|
updateWithValue: nil,
|
||||||
func(o options) []string { return boolSliceOrNil(o.disableUpdate) },
|
updateNoValue: func(o options) (options, error) { o.checkConfig = true; return o, nil },
|
||||||
}
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return "", o.checkConfig },
|
||||||
var disableMemoryOptimizationArg = arg{
|
description: "Check configuration and exit.",
|
||||||
"Deprecated. Disable memory optimization.",
|
longName: "check-config",
|
||||||
"no-mem-optimization", "",
|
shortName: "",
|
||||||
nil, nil, func(_ options, _ string) (f effect, err error) {
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: func(o options) (options, error) { o.disableUpdate = true; return o, nil },
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return "", o.disableUpdate },
|
||||||
|
description: "Don't check for updates.",
|
||||||
|
longName: "no-check-update",
|
||||||
|
shortName: "",
|
||||||
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: nil,
|
||||||
|
effect: func(_ options, _ string) (f effect, err error) {
|
||||||
log.Info("warning: using --no-mem-optimization flag has no effect and is deprecated")
|
log.Info("warning: using --no-mem-optimization flag has no effect and is deprecated")
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
func(o options) []string { return nil },
|
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||||
}
|
description: "Deprecated. Disable memory optimization.",
|
||||||
|
longName: "no-mem-optimization",
|
||||||
var verboseArg = arg{
|
shortName: "",
|
||||||
"Enable verbose output.",
|
}, {
|
||||||
"verbose", "v",
|
|
||||||
nil, func(o options) (options, error) { o.verbose = true; return o, nil }, nil,
|
|
||||||
func(o options) []string { return boolSliceOrNil(o.verbose) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var glinetArg = arg{
|
|
||||||
"Run in GL-Inet compatibility mode.",
|
|
||||||
"glinet", "",
|
|
||||||
nil, func(o options) (options, error) { o.glinetMode = true; return o, nil }, nil,
|
|
||||||
func(o options) []string { return boolSliceOrNil(o.glinetMode) },
|
|
||||||
}
|
|
||||||
|
|
||||||
var versionArg = arg{
|
|
||||||
description: "Show the version and exit. Show more detailed version description with -v.",
|
|
||||||
longName: "version",
|
|
||||||
shortName: "",
|
|
||||||
updateWithValue: nil,
|
|
||||||
updateNoValue: nil,
|
|
||||||
effect: func(o options, exec string) (effect, error) {
|
|
||||||
return func() error {
|
|
||||||
if o.verbose {
|
|
||||||
fmt.Println(version.Verbose())
|
|
||||||
} else {
|
|
||||||
fmt.Println(version.Full())
|
|
||||||
}
|
|
||||||
os.Exit(0)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
serialize: func(o options) []string { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
var helpArg = arg{
|
|
||||||
"Print this help.",
|
|
||||||
"help", "",
|
|
||||||
nil, nil, func(o options, exec string) (effect, error) {
|
|
||||||
return func() error { _ = printHelp(exec); os.Exit(64); return nil }, nil
|
|
||||||
},
|
|
||||||
func(o options) []string { return nil },
|
|
||||||
}
|
|
||||||
|
|
||||||
var noEtcHostsArg = arg{
|
|
||||||
description: "Deprecated. Do not use the OS-provided hosts.",
|
|
||||||
longName: "no-etc-hosts",
|
|
||||||
shortName: "",
|
|
||||||
updateWithValue: nil,
|
updateWithValue: nil,
|
||||||
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
updateNoValue: func(o options) (options, error) { o.noEtcHosts = true; return o, nil },
|
||||||
effect: func(_ options, _ string) (f effect, err error) {
|
effect: func(_ options, _ string) (f effect, err error) {
|
||||||
|
@ -242,146 +243,216 @@ var noEtcHostsArg = arg{
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
serialize: func(o options) []string { return boolSliceOrNil(o.noEtcHosts) },
|
serialize: func(o options) (val string, ok bool) { return "", o.noEtcHosts },
|
||||||
}
|
description: "Deprecated. Do not use the OS-provided hosts.",
|
||||||
|
longName: "no-etc-hosts",
|
||||||
var localFrontendArg = arg{
|
shortName: "",
|
||||||
description: "Use local frontend directories.",
|
}, {
|
||||||
longName: "local-frontend",
|
|
||||||
shortName: "",
|
|
||||||
updateWithValue: nil,
|
updateWithValue: nil,
|
||||||
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
|
updateNoValue: func(o options) (options, error) { o.localFrontend = true; return o, nil },
|
||||||
effect: nil,
|
effect: nil,
|
||||||
serialize: func(o options) []string { return boolSliceOrNil(o.localFrontend) },
|
serialize: func(o options) (val string, ok bool) { return "", o.localFrontend },
|
||||||
}
|
description: "Use local frontend directories.",
|
||||||
|
longName: "local-frontend",
|
||||||
|
shortName: "",
|
||||||
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: func(o options) (options, error) { o.verbose = true; return o, nil },
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return "", o.verbose },
|
||||||
|
description: "Enable verbose output.",
|
||||||
|
longName: "verbose",
|
||||||
|
shortName: "v",
|
||||||
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: func(o options) (options, error) { o.glinetMode = true; return o, nil },
|
||||||
|
effect: nil,
|
||||||
|
serialize: func(o options) (val string, ok bool) { return "", o.glinetMode },
|
||||||
|
description: "Run in GL-Inet compatibility mode.",
|
||||||
|
longName: "glinet",
|
||||||
|
shortName: "",
|
||||||
|
}, {
|
||||||
|
updateWithValue: nil,
|
||||||
|
updateNoValue: nil,
|
||||||
|
effect: func(o options, exec string) (effect, error) {
|
||||||
|
return func() error {
|
||||||
|
if o.verbose {
|
||||||
|
fmt.Println(version.Verbose())
|
||||||
|
} else {
|
||||||
|
fmt.Println(version.Full())
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
os.Exit(0)
|
||||||
args = []arg{
|
|
||||||
configArg,
|
|
||||||
workDirArg,
|
|
||||||
hostArg,
|
|
||||||
portArg,
|
|
||||||
serviceArg,
|
|
||||||
logfileArg,
|
|
||||||
pidfileArg,
|
|
||||||
checkConfigArg,
|
|
||||||
noCheckUpdateArg,
|
|
||||||
disableMemoryOptimizationArg,
|
|
||||||
noEtcHostsArg,
|
|
||||||
localFrontendArg,
|
|
||||||
verboseArg,
|
|
||||||
glinetArg,
|
|
||||||
versionArg,
|
|
||||||
helpArg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUsageLines(exec string, args []arg) []string {
|
return nil
|
||||||
usage := []string{
|
}, nil
|
||||||
"Usage:",
|
},
|
||||||
"",
|
serialize: func(o options) (val string, ok bool) { return "", false },
|
||||||
fmt.Sprintf("%s [options]", exec),
|
description: "Show the version and exit. Show more detailed version description with -v.",
|
||||||
"",
|
longName: "version",
|
||||||
"Options:",
|
shortName: "",
|
||||||
}
|
}}
|
||||||
for _, arg := range args {
|
|
||||||
|
// printHelp prints the entire help message. It exits with an error code if
|
||||||
|
// there are any I/O errors.
|
||||||
|
func printHelp(exec string) {
|
||||||
|
b := &strings.Builder{}
|
||||||
|
|
||||||
|
stringutil.WriteToBuilder(
|
||||||
|
b,
|
||||||
|
"Usage:\n\n",
|
||||||
|
fmt.Sprintf("%s [options]\n\n", exec),
|
||||||
|
"Options:\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, opt := range cmdLineOpts {
|
||||||
val := ""
|
val := ""
|
||||||
if arg.updateWithValue != nil {
|
if opt.updateWithValue != nil {
|
||||||
val = " VALUE"
|
val = " VALUE"
|
||||||
}
|
}
|
||||||
if arg.shortName != "" {
|
|
||||||
usage = append(usage, fmt.Sprintf(" -%s, %-30s %s",
|
longDesc := opt.longName + val
|
||||||
arg.shortName,
|
if opt.shortName != "" {
|
||||||
"--"+arg.longName+val,
|
_, err = fmt.Fprintf(b, " -%s, --%-28s %s\n", opt.shortName, longDesc, opt.description)
|
||||||
arg.description))
|
|
||||||
} else {
|
} else {
|
||||||
usage = append(usage, fmt.Sprintf(" %-34s %s",
|
_, err = fmt.Fprintf(b, " --%-32s %s\n", longDesc, opt.description)
|
||||||
"--"+arg.longName+val,
|
}
|
||||||
arg.description))
|
|
||||||
|
if err != nil {
|
||||||
|
// The only error here can be from incorrect Fprintf usage, which is
|
||||||
|
// a programmer error.
|
||||||
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return usage
|
|
||||||
|
_, err = fmt.Print(b)
|
||||||
|
if err != nil {
|
||||||
|
// Exit immediately, since not being able to print out a help message
|
||||||
|
// essentially means that the I/O is very broken at the moment.
|
||||||
|
exitWithError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func printHelp(exec string) error {
|
// parseCmdOpts parses the command-line arguments into options and effects.
|
||||||
for _, line := range getUsageLines(exec, args) {
|
func parseCmdOpts(cmdName string, args []string) (o options, eff effect, err error) {
|
||||||
_, err := fmt.Println(line)
|
// Don't use range since the loop changes the loop variable.
|
||||||
|
argsLen := len(args)
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
isKnown := false
|
||||||
|
for _, opt := range cmdLineOpts {
|
||||||
|
isKnown = argMatches(opt, arg)
|
||||||
|
if !isKnown {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.updateWithValue != nil {
|
||||||
|
i++
|
||||||
|
if i >= argsLen {
|
||||||
|
return o, eff, fmt.Errorf("got %s without argument", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
o, err = opt.updateWithValue(o, args[i])
|
||||||
|
} else {
|
||||||
|
o, eff, err = updateOptsNoValue(o, eff, opt, cmdName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return o, eff, fmt.Errorf("applying option %s: %w", arg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isKnown {
|
||||||
|
return o, eff, fmt.Errorf("unknown option %s", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o, eff, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// argMatches returns true if arg matches command-line option opt.
|
||||||
|
func argMatches(opt cmdLineOpt, arg string) (ok bool) {
|
||||||
|
if arg == "" || arg[0] != '-' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
arg = arg[1:]
|
||||||
|
if arg == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return (opt.shortName != "" && arg == opt.shortName) ||
|
||||||
|
(arg[0] == '-' && arg[1:] == opt.longName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateOptsNoValue sets values or effects from opt into o or prev.
|
||||||
|
func updateOptsNoValue(
|
||||||
|
o options,
|
||||||
|
prev effect,
|
||||||
|
opt cmdLineOpt,
|
||||||
|
cmdName string,
|
||||||
|
) (updated options, chained effect, err error) {
|
||||||
|
if opt.updateNoValue != nil {
|
||||||
|
o, err = opt.updateNoValue(o)
|
||||||
|
if err != nil {
|
||||||
|
return o, prev, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return o, prev, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
next, err := opt.effect(o, cmdName)
|
||||||
|
if err != nil {
|
||||||
|
return o, prev, err
|
||||||
|
}
|
||||||
|
|
||||||
|
chained = chainEffect(prev, next)
|
||||||
|
|
||||||
|
return o, chained, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// chainEffect chans the next effect after the prev one. If prev is nil, eff
|
||||||
|
// only calls next. If next is nil, eff is prev; if prev is nil, eff is next.
|
||||||
|
func chainEffect(prev, next effect) (eff effect) {
|
||||||
|
if prev == nil {
|
||||||
|
return next
|
||||||
|
} else if next == nil {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
|
eff = func() (err error) {
|
||||||
|
err = prev()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return next()
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
return eff
|
||||||
}
|
}
|
||||||
|
|
||||||
func argMatches(a arg, v string) bool {
|
// optsToArgs converts command line options into a list of arguments.
|
||||||
return v == "--"+a.longName || (a.shortName != "" && v == "-"+a.shortName)
|
func optsToArgs(o options) (args []string) {
|
||||||
}
|
for _, opt := range cmdLineOpts {
|
||||||
|
val, ok := opt.serialize(o)
|
||||||
func parse(exec string, ss []string) (o options, f effect, err error) {
|
if !ok {
|
||||||
for i := 0; i < len(ss); i++ {
|
continue
|
||||||
v := ss[i]
|
|
||||||
knownParam := false
|
|
||||||
for _, arg := range args {
|
|
||||||
if argMatches(arg, v) {
|
|
||||||
if arg.updateWithValue != nil {
|
|
||||||
if i+1 >= len(ss) {
|
|
||||||
return o, f, fmt.Errorf("got %s without argument", v)
|
|
||||||
}
|
|
||||||
i++
|
|
||||||
o, err = arg.updateWithValue(o, ss[i])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if arg.updateNoValue != nil {
|
|
||||||
o, err = arg.updateNoValue(o)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if arg.effect != nil {
|
|
||||||
var eff effect
|
|
||||||
eff, err = arg.effect(o, exec)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if eff != nil {
|
|
||||||
prevf := f
|
|
||||||
f = func() (ferr error) {
|
|
||||||
if prevf != nil {
|
|
||||||
ferr = prevf()
|
|
||||||
}
|
|
||||||
if ferr == nil {
|
|
||||||
ferr = eff()
|
|
||||||
}
|
|
||||||
return ferr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
knownParam = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !knownParam {
|
|
||||||
return o, f, fmt.Errorf("unknown option %v", v)
|
if opt.shortName != "" {
|
||||||
|
args = append(args, "-"+opt.shortName)
|
||||||
|
} else {
|
||||||
|
args = append(args, "--"+opt.longName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != "" {
|
||||||
|
args = append(args, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return args
|
||||||
}
|
|
||||||
|
|
||||||
func shortestFlag(a arg) string {
|
|
||||||
if a.shortName != "" {
|
|
||||||
return "-" + a.shortName
|
|
||||||
}
|
|
||||||
return "--" + a.longName
|
|
||||||
}
|
|
||||||
|
|
||||||
func serialize(o options) []string {
|
|
||||||
ss := []string{}
|
|
||||||
for _, arg := range args {
|
|
||||||
s := arg.serialize(o)
|
|
||||||
if s != nil {
|
|
||||||
ss = append(ss, append([]string{shortestFlag(arg)}, s...)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ss
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ package home
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -12,7 +12,7 @@ import (
|
||||||
func testParseOK(t *testing.T, ss ...string) options {
|
func testParseOK(t *testing.T, ss ...string) options {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
o, _, err := parse("", ss)
|
o, _, err := parseCmdOpts("", ss)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
return o
|
return o
|
||||||
|
@ -21,7 +21,7 @@ func testParseOK(t *testing.T, ss ...string) options {
|
||||||
func testParseErr(t *testing.T, descr string, ss ...string) {
|
func testParseErr(t *testing.T, descr string, ss ...string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
_, _, err := parse("", ss)
|
_, _, err := parseCmdOpts("", ss)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,11 +38,11 @@ func TestParseVerbose(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseConfigFilename(t *testing.T) {
|
func TestParseConfigFilename(t *testing.T) {
|
||||||
assert.Equal(t, "", testParseOK(t).configFilename, "empty is no config filename")
|
assert.Equal(t, "", testParseOK(t).confFilename, "empty is no config filename")
|
||||||
assert.Equal(t, "path", testParseOK(t, "-c", "path").configFilename, "-c is config filename")
|
assert.Equal(t, "path", testParseOK(t, "-c", "path").confFilename, "-c is config filename")
|
||||||
testParseParamMissing(t, "-c")
|
testParseParamMissing(t, "-c")
|
||||||
|
|
||||||
assert.Equal(t, "path", testParseOK(t, "--config", "path").configFilename, "--config is config filename")
|
assert.Equal(t, "path", testParseOK(t, "--config", "path").confFilename, "--config is config filename")
|
||||||
testParseParamMissing(t, "--config")
|
testParseParamMissing(t, "--config")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseBindHost(t *testing.T) {
|
func TestParseBindHost(t *testing.T) {
|
||||||
assert.Nil(t, testParseOK(t).bindHost, "empty is not host")
|
wantAddr := netip.MustParseAddr("1.2.3.4")
|
||||||
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
|
|
||||||
|
assert.Zero(t, testParseOK(t).bindHost, "empty is not host")
|
||||||
|
assert.Equal(t, wantAddr, testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
|
||||||
testParseParamMissing(t, "-h")
|
testParseParamMissing(t, "-h")
|
||||||
|
|
||||||
assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
|
assert.Equal(t, wantAddr, testParseOK(t, "--host", "1.2.3.4").bindHost, "--host is host")
|
||||||
testParseParamMissing(t, "--host")
|
testParseParamMissing(t, "--host")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +105,7 @@ func TestParseDisableUpdate(t *testing.T) {
|
||||||
|
|
||||||
// TODO(e.burkov): Remove after v0.108.0.
|
// TODO(e.burkov): Remove after v0.108.0.
|
||||||
func TestParseDisableMemoryOptimization(t *testing.T) {
|
func TestParseDisableMemoryOptimization(t *testing.T) {
|
||||||
o, eff, err := parse("", []string{"--no-mem-optimization"})
|
o, eff, err := parseCmdOpts("", []string{"--no-mem-optimization"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Nil(t, eff)
|
assert.Nil(t, eff)
|
||||||
|
@ -130,73 +132,73 @@ func TestParseUnknown(t *testing.T) {
|
||||||
testParseErr(t, "unknown dash", "-")
|
testParseErr(t, "unknown dash", "-")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSerialize(t *testing.T) {
|
func TestOptsToArgs(t *testing.T) {
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
|
args []string
|
||||||
opts options
|
opts options
|
||||||
ss []string
|
|
||||||
}{{
|
}{{
|
||||||
name: "empty",
|
name: "empty",
|
||||||
|
args: []string{},
|
||||||
opts: options{},
|
opts: options{},
|
||||||
ss: []string{},
|
|
||||||
}, {
|
}, {
|
||||||
name: "config_filename",
|
name: "config_filename",
|
||||||
opts: options{configFilename: "path"},
|
args: []string{"-c", "path"},
|
||||||
ss: []string{"-c", "path"},
|
opts: options{confFilename: "path"},
|
||||||
}, {
|
}, {
|
||||||
name: "work_dir",
|
name: "work_dir",
|
||||||
|
args: []string{"-w", "path"},
|
||||||
opts: options{workDir: "path"},
|
opts: options{workDir: "path"},
|
||||||
ss: []string{"-w", "path"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "bind_host",
|
name: "bind_host",
|
||||||
opts: options{bindHost: net.IP{1, 2, 3, 4}},
|
opts: options{bindHost: netip.MustParseAddr("1.2.3.4")},
|
||||||
ss: []string{"-h", "1.2.3.4"},
|
args: []string{"-h", "1.2.3.4"},
|
||||||
}, {
|
}, {
|
||||||
name: "bind_port",
|
name: "bind_port",
|
||||||
|
args: []string{"-p", "666"},
|
||||||
opts: options{bindPort: 666},
|
opts: options{bindPort: 666},
|
||||||
ss: []string{"-p", "666"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "log_file",
|
name: "log_file",
|
||||||
|
args: []string{"-l", "path"},
|
||||||
opts: options{logFile: "path"},
|
opts: options{logFile: "path"},
|
||||||
ss: []string{"-l", "path"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "pid_file",
|
name: "pid_file",
|
||||||
|
args: []string{"--pidfile", "path"},
|
||||||
opts: options{pidFile: "path"},
|
opts: options{pidFile: "path"},
|
||||||
ss: []string{"--pidfile", "path"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "disable_update",
|
name: "disable_update",
|
||||||
|
args: []string{"--no-check-update"},
|
||||||
opts: options{disableUpdate: true},
|
opts: options{disableUpdate: true},
|
||||||
ss: []string{"--no-check-update"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "control_action",
|
name: "control_action",
|
||||||
|
args: []string{"-s", "run"},
|
||||||
opts: options{serviceControlAction: "run"},
|
opts: options{serviceControlAction: "run"},
|
||||||
ss: []string{"-s", "run"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "glinet_mode",
|
name: "glinet_mode",
|
||||||
|
args: []string{"--glinet"},
|
||||||
opts: options{glinetMode: true},
|
opts: options{glinetMode: true},
|
||||||
ss: []string{"--glinet"},
|
|
||||||
}, {
|
}, {
|
||||||
name: "multiple",
|
name: "multiple",
|
||||||
opts: options{
|
args: []string{
|
||||||
serviceControlAction: "run",
|
|
||||||
configFilename: "config",
|
|
||||||
workDir: "work",
|
|
||||||
pidFile: "pid",
|
|
||||||
disableUpdate: true,
|
|
||||||
},
|
|
||||||
ss: []string{
|
|
||||||
"-c", "config",
|
"-c", "config",
|
||||||
"-w", "work",
|
"-w", "work",
|
||||||
"-s", "run",
|
"-s", "run",
|
||||||
"--pidfile", "pid",
|
"--pidfile", "pid",
|
||||||
"--no-check-update",
|
"--no-check-update",
|
||||||
},
|
},
|
||||||
|
opts: options{
|
||||||
|
serviceControlAction: "run",
|
||||||
|
confFilename: "config",
|
||||||
|
workDir: "work",
|
||||||
|
pidFile: "pid",
|
||||||
|
disableUpdate: true,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
result := serialize(tc.opts)
|
result := optsToArgs(tc.opts)
|
||||||
assert.ElementsMatch(t, tc.ss, result)
|
assert.ElementsMatch(t, tc.args, result)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -197,7 +197,7 @@ func handleServiceControlAction(opts options, clientBuildFS fs.FS) {
|
||||||
DisplayName: serviceDisplayName,
|
DisplayName: serviceDisplayName,
|
||||||
Description: serviceDescription,
|
Description: serviceDescription,
|
||||||
WorkingDirectory: pwd,
|
WorkingDirectory: pwd,
|
||||||
Arguments: serialize(runOpts),
|
Arguments: optsToArgs(runOpts),
|
||||||
}
|
}
|
||||||
configureService(svcConfig)
|
configureService(svcConfig)
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -28,216 +26,256 @@ import (
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var tlsWebHandlersRegistered = false
|
// tlsManager contains the current configuration and state of AdGuard Home TLS
|
||||||
|
// encryption.
|
||||||
|
type tlsManager struct {
|
||||||
|
// status is the current status of the configuration. It is never nil.
|
||||||
|
status *tlsConfigStatus
|
||||||
|
|
||||||
// TLSMod - TLS module object
|
// certLastMod is the last modification time of the certificate file.
|
||||||
type TLSMod struct {
|
certLastMod time.Time
|
||||||
certLastMod time.Time // last modification time of the certificate file
|
|
||||||
status tlsConfigStatus
|
confLock sync.Mutex
|
||||||
confLock sync.Mutex
|
conf tlsConfigSettings
|
||||||
conf tlsConfigSettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create TLS module
|
// newTLSManager initializes the TLS configuration.
|
||||||
func tlsCreate(conf tlsConfigSettings) *TLSMod {
|
func newTLSManager(conf tlsConfigSettings) (m *tlsManager, err error) {
|
||||||
t := &TLSMod{}
|
m = &tlsManager{
|
||||||
t.conf = conf
|
status: &tlsConfigStatus{},
|
||||||
if t.conf.Enabled {
|
conf: conf,
|
||||||
if !t.load() {
|
}
|
||||||
// Something is not valid - return an empty TLS config
|
|
||||||
return &TLSMod{conf: tlsConfigSettings{
|
if m.conf.Enabled {
|
||||||
Enabled: conf.Enabled,
|
err = m.load()
|
||||||
ServerName: conf.ServerName,
|
if err != nil {
|
||||||
PortHTTPS: conf.PortHTTPS,
|
return nil, err
|
||||||
PortDNSOverTLS: conf.PortDNSOverTLS,
|
|
||||||
PortDNSOverQUIC: conf.PortDNSOverQUIC,
|
|
||||||
AllowUnencryptedDoH: conf.AllowUnencryptedDoH,
|
|
||||||
}}
|
|
||||||
}
|
}
|
||||||
t.setCertFileTime()
|
|
||||||
|
m.setCertFileTime()
|
||||||
}
|
}
|
||||||
return t
|
|
||||||
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) load() bool {
|
// load reloads the TLS configuration from files or data from the config file.
|
||||||
if !tlsLoadConfig(&t.conf, &t.status) {
|
func (m *tlsManager) load() (err error) {
|
||||||
log.Error("failed to load TLS config: %s", t.status.WarningValidation)
|
err = loadTLSConf(&m.conf, m.status)
|
||||||
return false
|
if err != nil {
|
||||||
|
return fmt.Errorf("loading config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate current TLS config and update warnings (it could have been loaded from file)
|
return nil
|
||||||
data := validateCertificates(string(t.conf.CertificateChainData), string(t.conf.PrivateKeyData), t.conf.ServerName)
|
|
||||||
if !data.ValidPair {
|
|
||||||
log.Error("failed to validate certificate: %s", data.WarningValidation)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
t.status = data
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close - close module
|
|
||||||
func (t *TLSMod) Close() {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriteDiskConfig - write config
|
// WriteDiskConfig - write config
|
||||||
func (t *TLSMod) WriteDiskConfig(conf *tlsConfigSettings) {
|
func (m *tlsManager) WriteDiskConfig(conf *tlsConfigSettings) {
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
*conf = t.conf
|
*conf = m.conf
|
||||||
t.confLock.Unlock()
|
m.confLock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) setCertFileTime() {
|
// setCertFileTime sets t.certLastMod from the certificate. If there are
|
||||||
if len(t.conf.CertificatePath) == 0 {
|
// errors, setCertFileTime logs them.
|
||||||
|
func (m *tlsManager) setCertFileTime() {
|
||||||
|
if len(m.conf.CertificatePath) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fi, err := os.Stat(t.conf.CertificatePath)
|
|
||||||
|
fi, err := os.Stat(m.conf.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("TLS: %s", err)
|
log.Error("tls: looking up certificate path: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.certLastMod = fi.ModTime().UTC()
|
|
||||||
|
m.certLastMod = fi.ModTime().UTC()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start updates the configuration of TLSMod and starts it.
|
// start updates the configuration of t and starts it.
|
||||||
func (t *TLSMod) Start() {
|
func (m *tlsManager) start() {
|
||||||
if !tlsWebHandlersRegistered {
|
m.registerWebHandlers()
|
||||||
tlsWebHandlersRegistered = true
|
|
||||||
t.registerWebHandlers()
|
|
||||||
}
|
|
||||||
|
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
tlsConf := t.conf
|
tlsConf := m.conf
|
||||||
t.confLock.Unlock()
|
m.confLock.Unlock()
|
||||||
|
|
||||||
// The background context is used because the TLSConfigChanged wraps
|
// The background context is used because the TLSConfigChanged wraps context
|
||||||
// context with timeout on its own and shuts down the server, which
|
// with timeout on its own and shuts down the server, which handles current
|
||||||
// handles current request.
|
// request.
|
||||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload updates the configuration of TLSMod and restarts it.
|
// reload updates the configuration and restarts t.
|
||||||
func (t *TLSMod) Reload() {
|
func (m *tlsManager) reload() {
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
tlsConf := t.conf
|
tlsConf := m.conf
|
||||||
t.confLock.Unlock()
|
m.confLock.Unlock()
|
||||||
|
|
||||||
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
|
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fi, err := os.Stat(tlsConf.CertificatePath)
|
fi, err := os.Stat(tlsConf.CertificatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("TLS: %s", err)
|
log.Error("tls: %s", err)
|
||||||
return
|
|
||||||
}
|
|
||||||
if fi.ModTime().UTC().Equal(t.certLastMod) {
|
|
||||||
log.Debug("TLS: certificate file isn't modified")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Debug("TLS: certificate file is modified")
|
|
||||||
|
|
||||||
t.confLock.Lock()
|
|
||||||
r := t.load()
|
|
||||||
t.confLock.Unlock()
|
|
||||||
if !r {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
t.certLastMod = fi.ModTime().UTC()
|
if fi.ModTime().UTC().Equal(m.certLastMod) {
|
||||||
|
log.Debug("tls: certificate file isn't modified")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("tls: certificate file is modified")
|
||||||
|
|
||||||
|
m.confLock.Lock()
|
||||||
|
err = m.load()
|
||||||
|
m.confLock.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("tls: reloading: %s", err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m.certLastMod = fi.ModTime().UTC()
|
||||||
|
|
||||||
_ = reconfigureDNSServer()
|
_ = reconfigureDNSServer()
|
||||||
|
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
tlsConf = t.conf
|
tlsConf = m.conf
|
||||||
t.confLock.Unlock()
|
m.confLock.Unlock()
|
||||||
// The background context is used because the TLSConfigChanged wraps
|
|
||||||
// context with timeout on its own and shuts down the server, which
|
// The background context is used because the TLSConfigChanged wraps context
|
||||||
// handles current request.
|
// with timeout on its own and shuts down the server, which handles current
|
||||||
|
// request.
|
||||||
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
Context.web.TLSConfigChanged(context.Background(), tlsConf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set certificate and private key data
|
// loadTLSConf loads and validates the TLS configuration. The returned error is
|
||||||
func tlsLoadConfig(tls *tlsConfigSettings, status *tlsConfigStatus) bool {
|
// also set in status.WarningValidation.
|
||||||
tls.CertificateChainData = []byte(tls.CertificateChain)
|
func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error) {
|
||||||
tls.PrivateKeyData = []byte(tls.PrivateKey)
|
defer func() {
|
||||||
|
|
||||||
var err error
|
|
||||||
if tls.CertificatePath != "" {
|
|
||||||
if tls.CertificateChain != "" {
|
|
||||||
status.WarningValidation = "certificate data and file can't be set together"
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
tls.CertificateChainData, err = os.ReadFile(tls.CertificatePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.WarningValidation = err.Error()
|
status.WarningValidation = err.Error()
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
tlsConf.CertificateChainData = []byte(tlsConf.CertificateChain)
|
||||||
|
tlsConf.PrivateKeyData = []byte(tlsConf.PrivateKey)
|
||||||
|
|
||||||
|
if tlsConf.CertificatePath != "" {
|
||||||
|
if tlsConf.CertificateChain != "" {
|
||||||
|
return errors.Error("certificate data and file can't be set together")
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsConf.CertificateChainData, err = os.ReadFile(tlsConf.CertificatePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading cert file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
status.ValidCert = true
|
status.ValidCert = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if tls.PrivateKeyPath != "" {
|
if tlsConf.PrivateKeyPath != "" {
|
||||||
if tls.PrivateKey != "" {
|
if tlsConf.PrivateKey != "" {
|
||||||
status.WarningValidation = "private key data and file can't be set together"
|
return errors.Error("private key data and file can't be set together")
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
tls.PrivateKeyData, err = os.ReadFile(tls.PrivateKeyPath)
|
|
||||||
|
tlsConf.PrivateKeyData, err = os.ReadFile(tlsConf.PrivateKeyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
status.WarningValidation = err.Error()
|
return fmt.Errorf("reading key file: %w", err)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status.ValidKey = true
|
status.ValidKey = true
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
err = validateCertificates(
|
||||||
|
status,
|
||||||
|
tlsConf.CertificateChainData,
|
||||||
|
tlsConf.PrivateKeyData,
|
||||||
|
tlsConf.ServerName,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("validating certificate pair: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tlsConfigStatus contains the status of a certificate chain and key pair.
|
||||||
type tlsConfigStatus struct {
|
type tlsConfigStatus struct {
|
||||||
ValidCert bool `json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
|
// Subject is the subject of the first certificate in the chain.
|
||||||
ValidChain bool `json:"valid_chain"` // ValidChain is true if the specified certificates chain is verified and issued by a known CA
|
Subject string `json:"subject,omitempty"`
|
||||||
Subject string `json:"subject,omitempty"` // Subject is the subject of the first certificate in the chain
|
|
||||||
Issuer string `json:"issuer,omitempty"` // Issuer is the issuer of the first certificate in the chain
|
|
||||||
NotBefore time.Time `json:"not_before,omitempty"` // NotBefore is the NotBefore field of the first certificate in the chain
|
|
||||||
NotAfter time.Time `json:"not_after,omitempty"` // NotAfter is the NotAfter field of the first certificate in the chain
|
|
||||||
DNSNames []string `json:"dns_names"` // DNSNames is the value of SubjectAltNames field of the first certificate in the chain
|
|
||||||
|
|
||||||
// key status
|
// Issuer is the issuer of the first certificate in the chain.
|
||||||
ValidKey bool `json:"valid_key"` // ValidKey is true if the key is a valid private key
|
Issuer string `json:"issuer,omitempty"`
|
||||||
KeyType string `json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
|
|
||||||
|
|
||||||
// is usable? set by validator
|
// KeyType is the type of the private key.
|
||||||
ValidPair bool `json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
|
KeyType string `json:"key_type,omitempty"`
|
||||||
|
|
||||||
// warnings
|
// NotBefore is the NotBefore field of the first certificate in the chain.
|
||||||
WarningValidation string `json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
|
NotBefore time.Time `json:"not_before,omitempty"`
|
||||||
|
|
||||||
|
// NotAfter is the NotAfter field of the first certificate in the chain.
|
||||||
|
NotAfter time.Time `json:"not_after,omitempty"`
|
||||||
|
|
||||||
|
// WarningValidation is a validation warning message with the issue
|
||||||
|
// description.
|
||||||
|
WarningValidation string `json:"warning_validation,omitempty"`
|
||||||
|
|
||||||
|
// DNSNames is the value of SubjectAltNames field of the first certificate
|
||||||
|
// in the chain.
|
||||||
|
DNSNames []string `json:"dns_names"`
|
||||||
|
|
||||||
|
// ValidCert is true if the specified certificate chain is a valid chain of
|
||||||
|
// X509 certificates.
|
||||||
|
ValidCert bool `json:"valid_cert"`
|
||||||
|
|
||||||
|
// ValidChain is true if the specified certificate chain is verified and
|
||||||
|
// issued by a known CA.
|
||||||
|
ValidChain bool `json:"valid_chain"`
|
||||||
|
|
||||||
|
// ValidKey is true if the key is a valid private key.
|
||||||
|
ValidKey bool `json:"valid_key"`
|
||||||
|
|
||||||
|
// ValidPair is true if both certificate and private key are correct for
|
||||||
|
// each other.
|
||||||
|
ValidPair bool `json:"valid_pair"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// field ordering is important -- yaml fields will mirror ordering from here
|
// tlsConfig is the TLS configuration and status response.
|
||||||
type tlsConfig struct {
|
type tlsConfig struct {
|
||||||
tlsConfigStatus `json:",inline"`
|
*tlsConfigStatus `json:",inline"`
|
||||||
tlsConfigSettingsExt `json:",inline"`
|
tlsConfigSettingsExt `json:",inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// tlsConfigSettingsExt is used to (un)marshal PrivateKeySaved to ensure that
|
// tlsConfigSettingsExt is used to (un)marshal the PrivateKeySaved field to
|
||||||
// clients don't send and receive previously saved private keys.
|
// ensure that clients don't send and receive previously saved private keys.
|
||||||
type tlsConfigSettingsExt struct {
|
type tlsConfigSettingsExt struct {
|
||||||
tlsConfigSettings `json:",inline"`
|
tlsConfigSettings `json:",inline"`
|
||||||
// If private key saved as a string, we set this flag to true
|
|
||||||
// and omit key from answer.
|
// PrivateKeySaved is true if the private key is saved as a string and omit
|
||||||
|
// key from answer.
|
||||||
PrivateKeySaved bool `yaml:"-" json:"private_key_saved,inline"`
|
PrivateKeySaved bool `yaml:"-" json:"private_key_saved,inline"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
data := tlsConfig{
|
data := tlsConfig{
|
||||||
tlsConfigSettingsExt: tlsConfigSettingsExt{
|
tlsConfigSettingsExt: tlsConfigSettingsExt{
|
||||||
tlsConfigSettings: t.conf,
|
tlsConfigSettings: m.conf,
|
||||||
},
|
},
|
||||||
tlsConfigStatus: t.status,
|
tlsConfigStatus: m.status,
|
||||||
}
|
}
|
||||||
t.confLock.Unlock()
|
m.confLock.Unlock()
|
||||||
|
|
||||||
marshalTLS(w, r, data)
|
marshalTLS(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
func (m *tlsManager) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||||
setts, err := unmarshalTLS(r)
|
setts, err := unmarshalTLS(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||||
|
@ -246,7 +284,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if setts.PrivateKeySaved {
|
if setts.PrivateKeySaved {
|
||||||
setts.PrivateKey = t.conf.PrivateKey
|
setts.PrivateKey = m.conf.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if setts.Enabled {
|
if setts.Enabled {
|
||||||
|
@ -278,75 +316,74 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
status := tlsConfigStatus{}
|
// Skip the error check, since we are only interested in the value of
|
||||||
if tlsLoadConfig(&setts.tlsConfigSettings, &status) {
|
// status.WarningValidation.
|
||||||
status = validateCertificates(string(setts.CertificateChainData), string(setts.PrivateKeyData), setts.ServerName)
|
status := &tlsConfigStatus{}
|
||||||
}
|
_ = loadTLSConf(&setts.tlsConfigSettings, status)
|
||||||
|
resp := tlsConfig{
|
||||||
data := tlsConfig{
|
|
||||||
tlsConfigSettingsExt: setts,
|
tlsConfigSettingsExt: setts,
|
||||||
tlsConfigStatus: status,
|
tlsConfigStatus: status,
|
||||||
}
|
}
|
||||||
|
|
||||||
marshalTLS(w, r, data)
|
marshalTLS(w, r, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) setConfig(newConf tlsConfigSettings, status tlsConfigStatus) (restartHTTPS bool) {
|
func (m *tlsManager) setConfig(newConf tlsConfigSettings, status *tlsConfigStatus) (restartHTTPS bool) {
|
||||||
t.confLock.Lock()
|
m.confLock.Lock()
|
||||||
defer t.confLock.Unlock()
|
defer m.confLock.Unlock()
|
||||||
|
|
||||||
// Reset the DNSCrypt data before comparing, since we currently do not
|
// Reset the DNSCrypt data before comparing, since we currently do not
|
||||||
// accept these from the frontend.
|
// accept these from the frontend.
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
|
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
|
||||||
newConf.DNSCryptConfigFile = t.conf.DNSCryptConfigFile
|
newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
|
||||||
newConf.PortDNSCrypt = t.conf.PortDNSCrypt
|
newConf.PortDNSCrypt = m.conf.PortDNSCrypt
|
||||||
if !cmp.Equal(t.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
|
||||||
log.Info("tls config has changed, restarting https server")
|
log.Info("tls config has changed, restarting https server")
|
||||||
restartHTTPS = true
|
restartHTTPS = true
|
||||||
} else {
|
} else {
|
||||||
log.Info("tls config has not changed")
|
log.Info("tls: config has not changed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: don't do just `t.conf = data` because we must preserve all other members of t.conf
|
// Note: don't do just `t.conf = data` because we must preserve all other members of t.conf
|
||||||
t.conf.Enabled = newConf.Enabled
|
m.conf.Enabled = newConf.Enabled
|
||||||
t.conf.ServerName = newConf.ServerName
|
m.conf.ServerName = newConf.ServerName
|
||||||
t.conf.ForceHTTPS = newConf.ForceHTTPS
|
m.conf.ForceHTTPS = newConf.ForceHTTPS
|
||||||
t.conf.PortHTTPS = newConf.PortHTTPS
|
m.conf.PortHTTPS = newConf.PortHTTPS
|
||||||
t.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
|
||||||
t.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
|
||||||
t.conf.CertificateChain = newConf.CertificateChain
|
m.conf.CertificateChain = newConf.CertificateChain
|
||||||
t.conf.CertificatePath = newConf.CertificatePath
|
m.conf.CertificatePath = newConf.CertificatePath
|
||||||
t.conf.CertificateChainData = newConf.CertificateChainData
|
m.conf.CertificateChainData = newConf.CertificateChainData
|
||||||
t.conf.PrivateKey = newConf.PrivateKey
|
m.conf.PrivateKey = newConf.PrivateKey
|
||||||
t.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
m.conf.PrivateKeyPath = newConf.PrivateKeyPath
|
||||||
t.conf.PrivateKeyData = newConf.PrivateKeyData
|
m.conf.PrivateKeyData = newConf.PrivateKeyData
|
||||||
t.status = status
|
m.status = status
|
||||||
|
|
||||||
return restartHTTPS
|
return restartHTTPS
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
data, err := unmarshalTLS(r)
|
req, err := unmarshalTLS(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.PrivateKeySaved {
|
if req.PrivateKeySaved {
|
||||||
data.PrivateKey = t.conf.PrivateKey
|
req.PrivateKey = m.conf.PrivateKey
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Enabled {
|
if req.Enabled {
|
||||||
err = validatePorts(
|
err = validatePorts(
|
||||||
tcpPort(config.BindPort),
|
tcpPort(config.BindPort),
|
||||||
tcpPort(config.BetaBindPort),
|
tcpPort(config.BetaBindPort),
|
||||||
tcpPort(data.PortHTTPS),
|
tcpPort(req.PortHTTPS),
|
||||||
tcpPort(data.PortDNSOverTLS),
|
tcpPort(req.PortDNSOverTLS),
|
||||||
tcpPort(data.PortDNSCrypt),
|
tcpPort(req.PortDNSCrypt),
|
||||||
udpPort(config.DNS.Port),
|
udpPort(config.DNS.Port),
|
||||||
udpPort(data.PortDNSOverQUIC),
|
udpPort(req.PortDNSOverQUIC),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
|
||||||
|
@ -356,33 +393,33 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(e.burkov): Investigate and perhaps check other ports.
|
// TODO(e.burkov): Investigate and perhaps check other ports.
|
||||||
if !webCheckPortAvailable(data.PortHTTPS) {
|
if !webCheckPortAvailable(req.PortHTTPS) {
|
||||||
aghhttp.Error(
|
aghhttp.Error(
|
||||||
r,
|
r,
|
||||||
w,
|
w,
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
"port %d is not available, cannot enable HTTPS on it",
|
"port %d is not available, cannot enable https on it",
|
||||||
data.PortHTTPS,
|
req.PortHTTPS,
|
||||||
)
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
status := tlsConfigStatus{}
|
status := &tlsConfigStatus{}
|
||||||
if !tlsLoadConfig(&data.tlsConfigSettings, &status) {
|
err = loadTLSConf(&req.tlsConfigSettings, status)
|
||||||
data2 := tlsConfig{
|
if err != nil {
|
||||||
tlsConfigSettingsExt: data,
|
resp := tlsConfig{
|
||||||
tlsConfigStatus: t.status,
|
tlsConfigSettingsExt: req,
|
||||||
|
tlsConfigStatus: status,
|
||||||
}
|
}
|
||||||
marshalTLS(w, r, data2)
|
|
||||||
|
marshalTLS(w, r, resp)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
|
restartHTTPS := m.setConfig(req.tlsConfigSettings, status)
|
||||||
|
m.setCertFileTime()
|
||||||
restartHTTPS := t.setConfig(data.tlsConfigSettings, status)
|
|
||||||
t.setCertFileTime()
|
|
||||||
onConfigModified()
|
onConfigModified()
|
||||||
|
|
||||||
err = reconfigureDNSServer()
|
err = reconfigureDNSServer()
|
||||||
|
@ -392,12 +429,12 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data2 := tlsConfig{
|
resp := tlsConfig{
|
||||||
tlsConfigSettingsExt: data,
|
tlsConfigSettingsExt: req,
|
||||||
tlsConfigStatus: t.status,
|
tlsConfigStatus: m.status,
|
||||||
}
|
}
|
||||||
|
|
||||||
marshalTLS(w, r, data2)
|
marshalTLS(w, r, resp)
|
||||||
if f, ok := w.(http.Flusher); ok {
|
if f, ok := w.(http.Flusher); ok {
|
||||||
f.Flush()
|
f.Flush()
|
||||||
}
|
}
|
||||||
|
@ -408,7 +445,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
|
||||||
// same reason.
|
// same reason.
|
||||||
if restartHTTPS {
|
if restartHTTPS {
|
||||||
go func() {
|
go func() {
|
||||||
Context.web.TLSConfigChanged(context.Background(), data.tlsConfigSettings)
|
Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -445,89 +482,105 @@ func validatePorts(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyCertChain(data *tlsConfigStatus, certChain, serverName string) error {
|
// validateCertChain validates the certificate chain and sets data in status.
|
||||||
log.Tracef("TLS: got certificate: %d bytes", len(certChain))
|
// The returned error is also set in status.WarningValidation.
|
||||||
|
func validateCertChain(status *tlsConfigStatus, certChain []byte, serverName string) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
status.WarningValidation = err.Error()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// now do a more extended validation
|
log.Debug("tls: got certificate chain: %d bytes", len(certChain))
|
||||||
var certs []*pem.Block // PEM-encoded certificates
|
|
||||||
|
|
||||||
pemblock := []byte(certChain)
|
var certs []*pem.Block
|
||||||
|
pemblock := certChain
|
||||||
for {
|
for {
|
||||||
var decoded *pem.Block
|
var decoded *pem.Block
|
||||||
decoded, pemblock = pem.Decode(pemblock)
|
decoded, pemblock = pem.Decode(pemblock)
|
||||||
if decoded == nil {
|
if decoded == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if decoded.Type == "CERTIFICATE" {
|
if decoded.Type == "CERTIFICATE" {
|
||||||
certs = append(certs, decoded)
|
certs = append(certs, decoded)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var parsedCerts []*x509.Certificate
|
parsedCerts, err := parsePEMCerts(certs)
|
||||||
|
if err != nil {
|
||||||
for _, cert := range certs {
|
return err
|
||||||
parsed, err := x509.ParseCertificate(cert.Bytes)
|
|
||||||
if err != nil {
|
|
||||||
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
|
|
||||||
return errors.Error(data.WarningValidation)
|
|
||||||
}
|
|
||||||
parsedCerts = append(parsedCerts, parsed)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parsedCerts) == 0 {
|
status.ValidCert = true
|
||||||
data.WarningValidation = "You have specified an empty certificate"
|
|
||||||
return errors.Error(data.WarningValidation)
|
|
||||||
}
|
|
||||||
|
|
||||||
data.ValidCert = true
|
|
||||||
|
|
||||||
// spew.Dump(parsedCerts)
|
|
||||||
|
|
||||||
opts := x509.VerifyOptions{
|
opts := x509.VerifyOptions{
|
||||||
DNSName: serverName,
|
DNSName: serverName,
|
||||||
Roots: Context.tlsRoots,
|
Roots: Context.tlsRoots,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("number of certs - %d", len(parsedCerts))
|
log.Info("tls: number of certs: %d", len(parsedCerts))
|
||||||
if len(parsedCerts) > 1 {
|
|
||||||
// set up an intermediate
|
pool := x509.NewCertPool()
|
||||||
pool := x509.NewCertPool()
|
for _, cert := range parsedCerts[1:] {
|
||||||
for _, cert := range parsedCerts[1:] {
|
log.Info("tls: got an intermediate cert")
|
||||||
log.Printf("got an intermediate cert")
|
pool.AddCert(cert)
|
||||||
pool.AddCert(cert)
|
|
||||||
}
|
|
||||||
opts.Intermediates = pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
|
opts.Intermediates = pool
|
||||||
|
|
||||||
mainCert := parsedCerts[0]
|
mainCert := parsedCerts[0]
|
||||||
_, err := mainCert.Verify(opts)
|
_, err = mainCert.Verify(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// let self-signed certs through
|
// Let self-signed certs through and don't return this error.
|
||||||
data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
|
status.WarningValidation = fmt.Sprintf("certificate does not verify: %s", err)
|
||||||
} else {
|
} else {
|
||||||
data.ValidChain = true
|
status.ValidChain = true
|
||||||
}
|
}
|
||||||
// spew.Dump(chains)
|
|
||||||
|
|
||||||
// update status
|
|
||||||
if mainCert != nil {
|
if mainCert != nil {
|
||||||
notAfter := mainCert.NotAfter
|
status.Subject = mainCert.Subject.String()
|
||||||
data.Subject = mainCert.Subject.String()
|
status.Issuer = mainCert.Issuer.String()
|
||||||
data.Issuer = mainCert.Issuer.String()
|
status.NotAfter = mainCert.NotAfter
|
||||||
data.NotAfter = notAfter
|
status.NotBefore = mainCert.NotBefore
|
||||||
data.NotBefore = mainCert.NotBefore
|
status.DNSNames = mainCert.DNSNames
|
||||||
data.DNSNames = mainCert.DNSNames
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validatePkey(data *tlsConfigStatus, pkey string) error {
|
// parsePEMCerts parses multiple PEM-encoded certificates.
|
||||||
// now do a more extended validation
|
func parsePEMCerts(certs []*pem.Block) (parsedCerts []*x509.Certificate, err error) {
|
||||||
var key *pem.Block // PEM-encoded certificates
|
for i, cert := range certs {
|
||||||
|
var parsed *x509.Certificate
|
||||||
|
parsed, err = x509.ParseCertificate(cert.Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parsing certificate at index %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
// go through all pem blocks, but take first valid pem block and drop the rest
|
parsedCerts = append(parsedCerts, parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(parsedCerts) == 0 {
|
||||||
|
return nil, errors.Error("empty certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedCerts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePKey validates the private key and sets data in status. The returned
|
||||||
|
// error is also set in status.WarningValidation.
|
||||||
|
func validatePKey(status *tlsConfigStatus, pkey []byte) (err error) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
status.WarningValidation = err.Error()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
var key *pem.Block
|
||||||
|
|
||||||
|
// Go through all pem blocks, but take first valid pem block and drop the
|
||||||
|
// rest.
|
||||||
pemblock := []byte(pkey)
|
pemblock := []byte(pkey)
|
||||||
for {
|
for {
|
||||||
var decoded *pem.Block
|
var decoded *pem.Block
|
||||||
|
@ -544,61 +597,77 @@ func validatePkey(data *tlsConfigStatus, pkey string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if key == nil {
|
if key == nil {
|
||||||
data.WarningValidation = "No valid keys were found"
|
return errors.Error("no valid keys were found")
|
||||||
|
|
||||||
return errors.Error(data.WarningValidation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse the decoded key
|
|
||||||
_, keyType, err := parsePrivateKey(key.Bytes)
|
_, keyType, err := parsePrivateKey(key.Bytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", err)
|
return fmt.Errorf("parsing private key: %w", err)
|
||||||
|
|
||||||
return errors.Error(data.WarningValidation)
|
|
||||||
} else if keyType == keyTypeED25519 {
|
|
||||||
data.WarningValidation = "ED25519 keys are not supported by browsers; " +
|
|
||||||
"did you mean to use X25519 for key exchange?"
|
|
||||||
|
|
||||||
return errors.Error(data.WarningValidation)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.ValidKey = true
|
if keyType == keyTypeED25519 {
|
||||||
data.KeyType = keyType
|
return errors.Error(
|
||||||
|
"ED25519 keys are not supported by browsers; " +
|
||||||
|
"did you mean to use X25519 for key exchange?",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
status.ValidKey = true
|
||||||
|
status.KeyType = keyType
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateCertificates processes certificate data and its private key. All
|
// validateCertificates processes certificate data and its private key. All
|
||||||
// parameters are optional. On error, validateCertificates returns a partially
|
// parameters are optional. status must not be nil. The returned error is also
|
||||||
// set object with field WarningValidation containing error description.
|
// set in status.WarningValidation.
|
||||||
func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
|
func validateCertificates(
|
||||||
var data tlsConfigStatus
|
status *tlsConfigStatus,
|
||||||
|
certChain []byte,
|
||||||
// check only public certificate separately from the key
|
pkey []byte,
|
||||||
if certChain != "" {
|
serverName string,
|
||||||
if verifyCertChain(&data, certChain, serverName) != nil {
|
) (err error) {
|
||||||
return data
|
defer func() {
|
||||||
|
// Capitalize the warning for the UI. Assume that warnings are all
|
||||||
|
// ASCII-only.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Figure out a better way to do this. Perhaps a
|
||||||
|
// custom string or error type.
|
||||||
|
if w := status.WarningValidation; w != "" {
|
||||||
|
status.WarningValidation = strings.ToUpper(w[:1]) + w[1:]
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
// validate private key (right now the only validation possible is just parsing it)
|
// Check only the public certificate separately from the key.
|
||||||
if pkey != "" {
|
if len(certChain) > 0 {
|
||||||
if validatePkey(&data, pkey) != nil {
|
err = validateCertChain(status, certChain, serverName)
|
||||||
return data
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if both are set, validate both in unison
|
|
||||||
if pkey != "" && certChain != "" {
|
|
||||||
_, err := tls.X509KeyPair([]byte(certChain), []byte(pkey))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
|
return err
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
data.ValidPair = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
// Validate the private key by parsing it.
|
||||||
|
if len(pkey) > 0 {
|
||||||
|
err = validatePKey(status, pkey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If both are set, validate together.
|
||||||
|
if len(certChain) > 0 && len(pkey) > 0 {
|
||||||
|
_, err = tls.X509KeyPair(certChain, pkey)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("certificate-key pair: %w", err)
|
||||||
|
status.WarningValidation = err.Error()
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
status.ValidPair = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Key types.
|
// Key types.
|
||||||
|
@ -693,52 +762,9 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
|
||||||
_ = aghhttp.WriteJSONResponse(w, r, data)
|
_ = aghhttp.WriteJSONResponse(w, r, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registerWebHandlers registers HTTP handlers for TLS configuration
|
// registerWebHandlers registers HTTP handlers for TLS configuration.
|
||||||
func (t *TLSMod) registerWebHandlers() {
|
func (m *tlsManager) registerWebHandlers() {
|
||||||
httpRegister(http.MethodGet, "/control/tls/status", t.handleTLSStatus)
|
httpRegister(http.MethodGet, "/control/tls/status", m.handleTLSStatus)
|
||||||
httpRegister(http.MethodPost, "/control/tls/configure", t.handleTLSConfigure)
|
httpRegister(http.MethodPost, "/control/tls/configure", m.handleTLSConfigure)
|
||||||
httpRegister(http.MethodPost, "/control/tls/validate", t.handleTLSValidate)
|
httpRegister(http.MethodPost, "/control/tls/validate", m.handleTLSValidate)
|
||||||
}
|
|
||||||
|
|
||||||
// LoadSystemRootCAs tries to load root certificates from the operating system.
|
|
||||||
// It returns nil in case nothing is found so that that Go.crypto will use it's
|
|
||||||
// default algorithm to find system root CA list.
|
|
||||||
//
|
|
||||||
// See https://github.com/AdguardTeam/AdGuardHome/internal/issues/1311.
|
|
||||||
func LoadSystemRootCAs() (roots *x509.CertPool) {
|
|
||||||
// TODO(e.burkov): Use build tags instead.
|
|
||||||
if runtime.GOOS != "linux" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directories with the system root certificates, which aren't supported
|
|
||||||
// by Go.crypto.
|
|
||||||
dirs := []string{
|
|
||||||
// Entware.
|
|
||||||
"/opt/etc/ssl/certs",
|
|
||||||
}
|
|
||||||
roots = x509.NewCertPool()
|
|
||||||
for _, dir := range dirs {
|
|
||||||
dirEnts, err := os.ReadDir(dir)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
continue
|
|
||||||
} else if err != nil {
|
|
||||||
log.Error("opening directory: %q: %s", dir, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var rootsAdded bool
|
|
||||||
for _, de := range dirEnts {
|
|
||||||
var certData []byte
|
|
||||||
certData, err = os.ReadFile(filepath.Join(dir, de.Name()))
|
|
||||||
if err == nil && roots.AppendCertsFromPEM(certData) {
|
|
||||||
rootsAdded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rootsAdded {
|
|
||||||
return roots
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,7 @@ import (
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
var testCertChainData = []byte(`-----BEGIN CERTIFICATE-----
|
||||||
CertificateChain = `-----BEGIN CERTIFICATE-----
|
|
||||||
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
|
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
|
||||||
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
|
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
|
||||||
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
|
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
|
||||||
|
@ -21,8 +20,9 @@ eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8
|
||||||
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
|
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
|
||||||
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
|
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
|
||||||
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
|
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
|
||||||
-----END CERTIFICATE-----`
|
-----END CERTIFICATE-----`)
|
||||||
PrivateKey = `-----BEGIN PRIVATE KEY-----
|
|
||||||
|
var testPrivateKeyData = []byte(`-----BEGIN PRIVATE KEY-----
|
||||||
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
|
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
|
||||||
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
|
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
|
||||||
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
|
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
|
||||||
|
@ -37,36 +37,43 @@ An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp
|
||||||
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
|
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
|
||||||
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
|
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
|
||||||
kXS9jgARhhiWXJrk
|
kXS9jgARhhiWXJrk
|
||||||
-----END PRIVATE KEY-----`
|
-----END PRIVATE KEY-----`)
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateCertificates(t *testing.T) {
|
func TestValidateCertificates(t *testing.T) {
|
||||||
t.Run("bad_certificate", func(t *testing.T) {
|
t.Run("bad_certificate", func(t *testing.T) {
|
||||||
data := validateCertificates("bad cert", "", "")
|
status := &tlsConfigStatus{}
|
||||||
assert.NotEmpty(t, data.WarningValidation)
|
err := validateCertificates(status, []byte("bad cert"), nil, "")
|
||||||
assert.False(t, data.ValidCert)
|
assert.Error(t, err)
|
||||||
assert.False(t, data.ValidChain)
|
assert.NotEmpty(t, status.WarningValidation)
|
||||||
|
assert.False(t, status.ValidCert)
|
||||||
|
assert.False(t, status.ValidChain)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("bad_private_key", func(t *testing.T) {
|
t.Run("bad_private_key", func(t *testing.T) {
|
||||||
data := validateCertificates("", "bad priv key", "")
|
status := &tlsConfigStatus{}
|
||||||
assert.NotEmpty(t, data.WarningValidation)
|
err := validateCertificates(status, nil, []byte("bad priv key"), "")
|
||||||
assert.False(t, data.ValidKey)
|
assert.Error(t, err)
|
||||||
|
assert.NotEmpty(t, status.WarningValidation)
|
||||||
|
assert.False(t, status.ValidKey)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("valid", func(t *testing.T) {
|
t.Run("valid", func(t *testing.T) {
|
||||||
data := validateCertificates(CertificateChain, PrivateKey, "")
|
status := &tlsConfigStatus{}
|
||||||
notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z")
|
err := validateCertificates(status, testCertChainData, testPrivateKeyData, "")
|
||||||
notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z")
|
assert.NoError(t, err)
|
||||||
assert.NotEmpty(t, data.WarningValidation)
|
|
||||||
assert.True(t, data.ValidCert)
|
notBefore := time.Date(2019, 2, 27, 9, 24, 23, 0, time.UTC)
|
||||||
assert.False(t, data.ValidChain)
|
notAfter := time.Date(2046, 7, 14, 9, 24, 23, 0, time.UTC)
|
||||||
assert.True(t, data.ValidKey)
|
|
||||||
assert.Equal(t, "RSA", data.KeyType)
|
assert.NotEmpty(t, status.WarningValidation)
|
||||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Subject)
|
assert.True(t, status.ValidCert)
|
||||||
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Issuer)
|
assert.False(t, status.ValidChain)
|
||||||
assert.Equal(t, notBefore, data.NotBefore)
|
assert.True(t, status.ValidKey)
|
||||||
assert.Equal(t, notAfter, data.NotAfter)
|
assert.Equal(t, "RSA", status.KeyType)
|
||||||
assert.True(t, data.ValidPair)
|
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", status.Subject)
|
||||||
|
assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", status.Issuer)
|
||||||
|
assert.Equal(t, notBefore, status.NotBefore)
|
||||||
|
assert.Equal(t, notAfter, status.NotAfter)
|
||||||
|
assert.True(t, status.ValidPair)
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -4,8 +4,8 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ type webConfig struct {
|
||||||
clientFS fs.FS
|
clientFS fs.FS
|
||||||
clientBetaFS fs.FS
|
clientBetaFS fs.FS
|
||||||
|
|
||||||
BindHost net.IP
|
BindHost netip.Addr
|
||||||
BindPort int
|
BindPort int
|
||||||
BetaBindPort int
|
BetaBindPort int
|
||||||
PortHTTPS int
|
PortHTTPS int
|
||||||
|
@ -137,8 +137,11 @@ func newWeb(conf *webConfig) (w *Web) {
|
||||||
//
|
//
|
||||||
// TODO(a.garipov): Adapt for HTTP/3.
|
// TODO(a.garipov): Adapt for HTTP/3.
|
||||||
func webCheckPortAvailable(port int) (ok bool) {
|
func webCheckPortAvailable(port int) (ok bool) {
|
||||||
return Context.web.httpsServer.server != nil ||
|
if Context.web.httpsServer.server != nil {
|
||||||
aghnet.CheckPort("tcp", config.BindHost, port) == nil
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Package agh contains common entities and interfaces of AdGuard Home.
|
||||||
|
package agh
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// Service is the interface for API servers.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider adding a context to Start.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider adding a Wait method or making an extension
|
||||||
|
// interface for that.
|
||||||
|
type Service interface {
|
||||||
|
// Start starts the service. It does not block.
|
||||||
|
Start() (err error)
|
||||||
|
|
||||||
|
// Shutdown gracefully stops the service. ctx is used to determine
|
||||||
|
// a timeout before trying to stop the service less gracefully.
|
||||||
|
Shutdown(ctx context.Context) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ Service = EmptyService{}
|
||||||
|
|
||||||
|
// EmptyService is a [Service] that does nothing.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
|
type EmptyService struct{}
|
||||||
|
|
||||||
|
// Start implements the [Service] interface for EmptyService.
|
||||||
|
func (EmptyService) Start() (err error) { return nil }
|
||||||
|
|
||||||
|
// Shutdown implements the [Service] interface for EmptyService.
|
||||||
|
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
|
||||||
|
|
||||||
|
// ServiceWithConfig is an extension of the [Service] interface for services
|
||||||
|
// that can return their configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Consider removing this generic interface if we figure out
|
||||||
|
// how to make it testable in a better way.
|
||||||
|
type ServiceWithConfig[ConfigType any] interface {
|
||||||
|
Service
|
||||||
|
|
||||||
|
Config() (c ConfigType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
|
||||||
|
|
||||||
|
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
|
||||||
|
// method returns Conf.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Remove if unnecessary.
|
||||||
|
type EmptyServiceWithConfig[ConfigType any] struct {
|
||||||
|
EmptyService
|
||||||
|
|
||||||
|
Conf ConfigType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config implements the [ServiceWithConfig] interface for
|
||||||
|
// *EmptyServiceWithConfig.
|
||||||
|
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
|
||||||
|
return s.Conf
|
||||||
|
}
|
|
@ -8,39 +8,49 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/netip"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/version"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main is the entry point of application.
|
// Main is the entry point of application.
|
||||||
func Main(clientBuildFS fs.FS) {
|
func Main(clientBuildFS fs.FS) {
|
||||||
// # Initial Configuration
|
// Initial Configuration
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
rand.Seed(start.UnixNano())
|
rand.Seed(start.UnixNano())
|
||||||
|
|
||||||
// TODO(a.garipov): Set up logging.
|
// TODO(a.garipov): Set up logging.
|
||||||
|
|
||||||
// # Web Service
|
log.Info("starting adguard home, version %s, pid %d", version.Version(), os.Getpid())
|
||||||
|
|
||||||
|
// Web Service
|
||||||
|
|
||||||
// TODO(a.garipov): Use in the Web service.
|
// TODO(a.garipov): Use in the Web service.
|
||||||
_ = clientBuildFS
|
_ = clientBuildFS
|
||||||
|
|
||||||
// TODO(a.garipov): Make configurable.
|
// TODO(a.garipov): Set up configuration file name.
|
||||||
web := websvc.New(&websvc.Config{
|
const confFile = "AdGuardHome.1.yaml"
|
||||||
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:3001")},
|
|
||||||
Start: start,
|
|
||||||
Timeout: 60 * time.Second,
|
|
||||||
})
|
|
||||||
|
|
||||||
err := web.Start()
|
confMgr, err := configmgr.New(confFile, start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
fatalOnError(err)
|
fatalOnError(err)
|
||||||
|
|
||||||
sigHdlr := newSignalHandler(
|
sigHdlr := newSignalHandler(
|
||||||
|
confFile,
|
||||||
|
start,
|
||||||
web,
|
web,
|
||||||
|
dns,
|
||||||
)
|
)
|
||||||
|
|
||||||
go sigHdlr.handle()
|
go sigHdlr.handle()
|
|
@ -0,0 +1,118 @@
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// signalHandler processes incoming signals and shuts services down.
|
||||||
|
type signalHandler struct {
|
||||||
|
// signal is the channel to which OS signals are sent.
|
||||||
|
signal chan os.Signal
|
||||||
|
|
||||||
|
// confFile is the path to the configuration file.
|
||||||
|
confFile string
|
||||||
|
|
||||||
|
// start is the time at which AdGuard Home has been started.
|
||||||
|
start time.Time
|
||||||
|
|
||||||
|
// services are the services that are shut down before application exiting.
|
||||||
|
services []agh.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle processes OS signals.
|
||||||
|
func (h *signalHandler) handle() {
|
||||||
|
defer log.OnPanic("signalHandler.handle")
|
||||||
|
|
||||||
|
for sig := range h.signal {
|
||||||
|
log.Info("sighdlr: received signal %q", sig)
|
||||||
|
|
||||||
|
if aghos.IsReconfigureSignal(sig) {
|
||||||
|
h.reconfigure()
|
||||||
|
} else if aghos.IsShutdownSignal(sig) {
|
||||||
|
status := h.shutdown()
|
||||||
|
log.Info("sighdlr: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconfigure rereads the configuration file and updates and restarts services.
|
||||||
|
func (h *signalHandler) reconfigure() {
|
||||||
|
log.Info("sighdlr: reconfiguring adguard home")
|
||||||
|
|
||||||
|
status := h.shutdown()
|
||||||
|
if status != statusSuccess {
|
||||||
|
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
|
||||||
|
|
||||||
|
os.Exit(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): This is a very rough way to do it. Some services can be
|
||||||
|
// reconfigured without the full shutdown, and the error handling is
|
||||||
|
// currently not the best.
|
||||||
|
|
||||||
|
confMgr, err := configmgr.New(h.confFile, h.start)
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
web := confMgr.Web()
|
||||||
|
err = web.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
dns := confMgr.DNS()
|
||||||
|
err = dns.Start()
|
||||||
|
fatalOnError(err)
|
||||||
|
|
||||||
|
h.services = []agh.Service{
|
||||||
|
dns,
|
||||||
|
web,
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("sighdlr: successfully reconfigured adguard home")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit status constants.
|
||||||
|
const (
|
||||||
|
statusSuccess = 0
|
||||||
|
statusError = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
// shutdown gracefully shuts down all services.
|
||||||
|
func (h *signalHandler) shutdown() (status int) {
|
||||||
|
ctx, cancel := ctxWithDefaultTimeout()
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
status = statusSuccess
|
||||||
|
|
||||||
|
log.Info("sighdlr: shutting down services")
|
||||||
|
for i, service := range h.services {
|
||||||
|
err := service.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
|
||||||
|
status = statusError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
||||||
|
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
|
||||||
|
h = &signalHandler{
|
||||||
|
signal: make(chan os.Signal, 1),
|
||||||
|
confFile: confFile,
|
||||||
|
start: start,
|
||||||
|
services: svcs,
|
||||||
|
}
|
||||||
|
|
||||||
|
aghos.NotifyShutdownSignal(h.signal)
|
||||||
|
aghos.NotifyReconfigureSignal(h.signal)
|
||||||
|
|
||||||
|
return h
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/golibs/timeutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Structures
|
||||||
|
|
||||||
|
// config is the top-level on-disk configuration structure.
|
||||||
|
type config struct {
|
||||||
|
DNS *dnsConfig `yaml:"dns"`
|
||||||
|
HTTP *httpConfig `yaml:"http"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
SchemaVersion int `yaml:"schema_version"`
|
||||||
|
// TODO(a.garipov): Use.
|
||||||
|
DebugPprof bool `yaml:"debug_pprof"`
|
||||||
|
Verbose bool `yaml:"verbose"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// dnsConfig is the on-disk DNS configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type dnsConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
BootstrapDNS []string `yaml:"bootstrap_dns"`
|
||||||
|
UpstreamDNS []string `yaml:"upstream_dns"`
|
||||||
|
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// httpConfig is the on-disk web API configuration.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Validate.
|
||||||
|
type httpConfig struct {
|
||||||
|
Addresses []netip.AddrPort `yaml:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
|
||||||
|
Timeout timeutil.Duration `yaml:"timeout"`
|
||||||
|
ForceHTTPS bool `yaml:"force_https"`
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
// Package configmgr defines the AdGuard Home on-disk configuration entities and
|
||||||
|
// configuration manager.
|
||||||
|
package configmgr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Configuration Manager
|
||||||
|
|
||||||
|
// Manager handles full and partial changes in the configuration, persisting
|
||||||
|
// them to disk if necessary.
|
||||||
|
type Manager struct {
|
||||||
|
// updMu makes sure that at most one reconfiguration is performed at a time.
|
||||||
|
// updMu protects all fields below.
|
||||||
|
updMu *sync.RWMutex
|
||||||
|
|
||||||
|
// dns is the DNS service.
|
||||||
|
dns *dnssvc.Service
|
||||||
|
|
||||||
|
// Web is the Web API service.
|
||||||
|
web *websvc.Service
|
||||||
|
|
||||||
|
// current is the current configuration.
|
||||||
|
current *config
|
||||||
|
|
||||||
|
// fileName is the name of the configuration file.
|
||||||
|
fileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new *Manager that persists changes to the file pointed to by
|
||||||
|
// fileName. It reads the configuration file and populates the service fields.
|
||||||
|
// start is the startup time of AdGuard Home.
|
||||||
|
func New(fileName string, start time.Time) (m *Manager, err error) {
|
||||||
|
defer func() { err = errors.Annotate(err, "reading config") }()
|
||||||
|
|
||||||
|
conf := &config{}
|
||||||
|
f, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { err = errors.WithDeferred(err, f.Close()) }()
|
||||||
|
|
||||||
|
err = yaml.NewDecoder(f).Decode(conf)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Move into a separate function and add other logging
|
||||||
|
// settings.
|
||||||
|
if conf.Verbose {
|
||||||
|
log.SetLevel(log.DEBUG)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate the configuration structure. Return an error
|
||||||
|
// if it's incorrect.
|
||||||
|
|
||||||
|
m = &Manager{
|
||||||
|
updMu: &sync.RWMutex{},
|
||||||
|
current: conf,
|
||||||
|
fileName: fileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Get the context with the timeout from the arguments?
|
||||||
|
const assemblyTimeout = 5 * time.Second
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
err = m.assemble(ctx, conf, start)
|
||||||
|
if err != nil {
|
||||||
|
// Don't wrap the error, because it's informative enough as is.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// assemble creates all services and puts them into the corresponding fields.
|
||||||
|
// The fields of conf must not be modified after calling assemble.
|
||||||
|
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
|
||||||
|
dnsConf := &dnssvc.Config{
|
||||||
|
Addresses: conf.DNS.Addresses,
|
||||||
|
BootstrapServers: conf.DNS.BootstrapDNS,
|
||||||
|
UpstreamServers: conf.DNS.UpstreamDNS,
|
||||||
|
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
|
||||||
|
}
|
||||||
|
err = m.updateDNS(ctx, dnsConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
webSvcConf := &websvc.Config{
|
||||||
|
ConfigManager: m,
|
||||||
|
// TODO(a.garipov): Fill from config file.
|
||||||
|
TLS: nil,
|
||||||
|
Start: start,
|
||||||
|
Addresses: conf.HTTP.Addresses,
|
||||||
|
SecureAddresses: conf.HTTP.SecureAddresses,
|
||||||
|
Timeout: conf.HTTP.Timeout.Duration,
|
||||||
|
ForceHTTPS: conf.HTTP.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, webSvcConf)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("assembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNS returns the current DNS service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.dns
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateDNS.
|
||||||
|
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateDNS(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling dnssvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
|
||||||
|
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
if prev := m.dns; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down dns svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
svc, err := dnssvc.New(c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating dns svc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.dns = svc
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web returns the current web service. It is safe for concurrent use.
|
||||||
|
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
m.updMu.RLock()
|
||||||
|
defer m.updMu.RUnlock()
|
||||||
|
|
||||||
|
return m.web
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
|
||||||
|
// fields of c must not be modified after calling UpdateWeb.
|
||||||
|
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
m.updMu.Lock()
|
||||||
|
defer m.updMu.Unlock()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Update and write the configuration file. Return an
|
||||||
|
// error if something went wrong.
|
||||||
|
|
||||||
|
err = m.updateWeb(ctx, c)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reassembling websvc: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWeb recreates the web service. m.upd is expected to be locked.
|
||||||
|
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
if prev := m.web; prev != nil {
|
||||||
|
err = prev.Shutdown(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("shutting down web svc: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.web = websvc.New(c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
// TODO(a.garipov): Add a “dnsproxy proxy” package to shield us from changes
|
||||||
// and replacement of module dnsproxy.
|
// and replacement of module dnsproxy.
|
||||||
"github.com/AdguardTeam/dnsproxy/proxy"
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
||||||
|
@ -47,6 +48,14 @@ type Config struct {
|
||||||
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
// Service is the AdGuard Home DNS service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
// running is an atomic boolean value. Keep it the first value in the
|
||||||
|
// struct to ensure atomic alignment. 0 means that the service is not
|
||||||
|
// running, 1 means that it is running.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19 or get rid of it
|
||||||
|
// completely.
|
||||||
|
running uint64
|
||||||
|
|
||||||
proxy *proxy.Proxy
|
proxy *proxy.Proxy
|
||||||
bootstraps []string
|
bootstraps []string
|
||||||
upstreams []string
|
upstreams []string
|
||||||
|
@ -160,6 +169,17 @@ func (svc *Service) Start() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// TODO(a.garipov): [proxy.Proxy.Start] doesn't actually have any way to
|
||||||
|
// tell when all servers are actually up, so at best this is merely an
|
||||||
|
// assumption.
|
||||||
|
if err != nil {
|
||||||
|
atomic.StoreUint64(&svc.running, 0)
|
||||||
|
} else {
|
||||||
|
atomic.StoreUint64(&svc.running, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return svc.proxy.Start()
|
return svc.proxy.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,13 +193,27 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||||
return svc.proxy.Stop()
|
return svc.proxy.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config returns the current configuration of the web service.
|
// 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) {
|
func (svc *Service) Config() (c *Config) {
|
||||||
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
// TODO(a.garipov): Do we need to get the TCP addresses separately?
|
||||||
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
|
|
||||||
addrs := make([]netip.AddrPort, len(udpAddrs))
|
var addrs []netip.AddrPort
|
||||||
for i, a := range udpAddrs {
|
if atomic.LoadUint64(&svc.running) == 1 {
|
||||||
addrs[i] = a.(*net.UDPAddr).AddrPort()
|
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{
|
c = &Config{
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/dnssvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/dnsproxy/upstream"
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
||||||
"github.com/miekg/dns"
|
"github.com/miekg/dns"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
|
@ -0,0 +1,84 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsDNS struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
|
||||||
|
// DnsSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIDNSSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
BootstrapServers []string `json:"bootstrap_servers"`
|
||||||
|
UpstreamServers []string `json:"upstream_servers"`
|
||||||
|
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsDNS{
|
||||||
|
Addresses: []netip.AddrPort{},
|
||||||
|
BootstrapServers: []string{},
|
||||||
|
UpstreamServers: []string{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &dnssvc.Config{
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
BootstrapServers: req.BootstrapServers,
|
||||||
|
UpstreamServers: req.UpstreamServers,
|
||||||
|
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
err = svc.confMgr.UpdateDNS(ctx, newConf)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newSvc := svc.confMgr.DNS()
|
||||||
|
err = newSvc.Start()
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
BootstrapServers: newConf.BootstrapServers,
|
||||||
|
UpstreamServers: newConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsDNS(t *testing.T) {
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
|
||||||
|
BootstrapServers: []string{"1.0.0.1"},
|
||||||
|
UpstreamServers: []string{"1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
|
||||||
|
var numStarted uint64
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
|
||||||
|
OnStart: func() (err error) {
|
||||||
|
atomic.AddUint64(&numStarted, 1)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
|
||||||
|
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsDNS,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantDNS.Addresses,
|
||||||
|
"bootstrap_servers": wantDNS.BootstrapServers,
|
||||||
|
"upstream_servers": wantDNS.UpstreamServers,
|
||||||
|
"upstream_timeout": wantDNS.UpstreamTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIDNSSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, uint64(1), numStarted)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
assert.Equal(t, wantDNS, resp)
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP Settings Handlers
|
||||||
|
|
||||||
|
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
type ReqPatchSettingsHTTP struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Add wait time.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
|
||||||
|
// HttpSettings object in the OpenAPI specification.
|
||||||
|
type HTTPAPIHTTPSettings struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
Addresses []netip.AddrPort `json:"addresses"`
|
||||||
|
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
|
||||||
|
Timeout JSONDuration `json:"timeout"`
|
||||||
|
ForceHTTPS bool `json:"force_https"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
|
||||||
|
// HTTP API.
|
||||||
|
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &ReqPatchSettingsHTTP{}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Validate nulls and proper JSON patch.
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
if err != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConf := &Config{
|
||||||
|
ConfigManager: svc.confMgr,
|
||||||
|
TLS: svc.tls,
|
||||||
|
Addresses: req.Addresses,
|
||||||
|
SecureAddresses: req.SecureAddresses,
|
||||||
|
Timeout: time.Duration(req.Timeout),
|
||||||
|
ForceHTTPS: svc.forceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: newConf.Addresses,
|
||||||
|
SecureAddresses: newConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(newConf.Timeout),
|
||||||
|
ForceHTTPS: newConf.ForceHTTPS,
|
||||||
|
})
|
||||||
|
|
||||||
|
cancelUpd := func() {}
|
||||||
|
updCtx := context.Background()
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the new HTTP service in a separate goroutine to let this handler
|
||||||
|
// finish and thus, this server to shutdown.
|
||||||
|
go func() {
|
||||||
|
defer cancelUpd()
|
||||||
|
|
||||||
|
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
|
||||||
|
if updErr != nil {
|
||||||
|
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(a.garipov): Consider better ways to do this.
|
||||||
|
const maxUpdDur = 10 * time.Second
|
||||||
|
updStart := time.Now()
|
||||||
|
var newSvc agh.ServiceWithConfig[*Config]
|
||||||
|
for newSvc = svc.confMgr.Web(); newSvc == svc; {
|
||||||
|
if time.Since(updStart) >= maxUpdDur {
|
||||||
|
log.Error("websvc: failed to update svc after %s", maxUpdDur)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("websvc: waiting for new websvc to be configured")
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
updErr = newSvc.Start()
|
||||||
|
if updErr != nil {
|
||||||
|
log.Error("websvc: new svc failed to start with error: %s", updErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(10 * time.Second),
|
||||||
|
ForceHTTPS: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsHTTP,
|
||||||
|
}
|
||||||
|
|
||||||
|
req := jobj{
|
||||||
|
"addresses": wantWeb.Addresses,
|
||||||
|
"secure_addresses": wantWeb.SecureAddresses,
|
||||||
|
"timeout": wantWeb.Timeout,
|
||||||
|
"force_https": wantWeb.ForceHTTPS,
|
||||||
|
}
|
||||||
|
|
||||||
|
respBody := httpPatch(t, u, req, http.StatusOK)
|
||||||
|
resp := &websvc.HTTPAPIHTTPSettings{}
|
||||||
|
err := json.Unmarshal(respBody, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantWeb, resp)
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
|
||||||
|
"github.com/AdguardTeam/golibs/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON Utilities
|
||||||
|
|
||||||
|
// nsecPerMsec is the number of nanoseconds in a millisecond.
|
||||||
|
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
|
||||||
|
|
||||||
|
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
|
||||||
|
// into JSON according to our API conventions.
|
||||||
|
type JSONDuration time.Duration
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONDuration(0)
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
|
||||||
|
// always nil.
|
||||||
|
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Duration(d)) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONDuration)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
|
||||||
|
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if d == nil {
|
||||||
|
return fmt.Errorf("json duration is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*d = JSONDuration(int64(msec * nsecPerMsec))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
|
||||||
|
// according to our API conventions.
|
||||||
|
type JSONTime time.Time
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Marshaler = JSONTime{}
|
||||||
|
|
||||||
|
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
|
||||||
|
// always nil.
|
||||||
|
func (t JSONTime) MarshalJSON() (b []byte, err error) {
|
||||||
|
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
|
||||||
|
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ json.Unmarshaler = (*JSONTime)(nil)
|
||||||
|
|
||||||
|
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
|
||||||
|
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
if t == nil {
|
||||||
|
return fmt.Errorf("json time is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
msec, err := strconv.ParseFloat(string(b), 64)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("parsing json time: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
|
||||||
|
// and logs any errors it encounters. r is used to get additional information
|
||||||
|
// from the request.
|
||||||
|
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
|
||||||
|
writeJSONResponse(w, r, v, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONResponse writes headers with code, encodes v into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
|
||||||
|
// TODO(a.garipov): Put some of these to a middleware.
|
||||||
|
h := w.Header()
|
||||||
|
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
|
||||||
|
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
|
||||||
|
|
||||||
|
w.WriteHeader(code)
|
||||||
|
|
||||||
|
err := json.NewEncoder(w).Encode(v)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
|
||||||
|
// definition in the OpenAPI specification.
|
||||||
|
type ErrorCode string
|
||||||
|
|
||||||
|
// ErrorCode constants.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Expand and document codes.
|
||||||
|
const (
|
||||||
|
// ErrorCodeTMP000 is the temporary error code used for all errors.
|
||||||
|
ErrorCodeTMP000 = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
|
||||||
|
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
|
||||||
|
// specification.
|
||||||
|
type HTTPAPIErrorResp struct {
|
||||||
|
Code ErrorCode `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
|
||||||
|
// errors it encounters. r is used to get additional information from the
|
||||||
|
// request.
|
||||||
|
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
|
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
|
||||||
|
|
||||||
|
writeJSONResponse(w, r, &HTTPAPIErrorResp{
|
||||||
|
Code: ErrorCodeTMP000,
|
||||||
|
Msg: err.Error(),
|
||||||
|
}, http.StatusUnprocessableEntity)
|
||||||
|
}
|
|
@ -0,0 +1,114 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testJSONTime is the JSON time for tests.
|
||||||
|
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
|
||||||
|
|
||||||
|
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
|
||||||
|
const testJSONTimeStr = "1234567890123.456"
|
||||||
|
|
||||||
|
func TestJSONTime_MarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
in websvc.JSONTime
|
||||||
|
want []byte
|
||||||
|
}{{
|
||||||
|
name: "unix_zero",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime(time.Unix(0, 0)),
|
||||||
|
want: []byte("0"),
|
||||||
|
}, {
|
||||||
|
name: "empty",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: websvc.JSONTime{},
|
||||||
|
want: []byte("-6795364578871.345"),
|
||||||
|
}, {
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
in: testJSONTime,
|
||||||
|
want: []byte(testJSONTimeStr),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got, err := tc.in.MarshalJSON()
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
in := &struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}{
|
||||||
|
A: testJSONTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := json.Marshal(in)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONTime_UnmarshalJSON(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
wantErrMsg string
|
||||||
|
want websvc.JSONTime
|
||||||
|
data []byte
|
||||||
|
}{{
|
||||||
|
name: "time",
|
||||||
|
wantErrMsg: "",
|
||||||
|
want: testJSONTime,
|
||||||
|
data: []byte(testJSONTimeStr),
|
||||||
|
}, {
|
||||||
|
name: "bad",
|
||||||
|
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
|
||||||
|
`invalid syntax`,
|
||||||
|
want: websvc.JSONTime{},
|
||||||
|
data: []byte(`{}`),
|
||||||
|
}}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
var got websvc.JSONTime
|
||||||
|
err := got.UnmarshalJSON(tc.data)
|
||||||
|
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
|
||||||
|
|
||||||
|
assert.Equal(t, tc.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
msg := err.Error()
|
||||||
|
assert.Equal(t, "json time is nil", msg)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("json", func(t *testing.T) {
|
||||||
|
want := testJSONTime
|
||||||
|
var got struct {
|
||||||
|
A websvc.JSONTime
|
||||||
|
}
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, want, got.A)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
// Path constants
|
||||||
|
const (
|
||||||
|
PathHealthCheck = "/health-check"
|
||||||
|
|
||||||
|
PathV1SettingsAll = "/api/v1/settings/all"
|
||||||
|
PathV1SettingsDNS = "/api/v1/settings/dns"
|
||||||
|
PathV1SettingsHTTP = "/api/v1/settings/http"
|
||||||
|
PathV1SystemInfo = "/api/v1/system/info"
|
||||||
|
)
|
|
@ -0,0 +1,42 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// All Settings Handlers
|
||||||
|
|
||||||
|
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
|
||||||
|
// HTTP API.
|
||||||
|
type RespGetV1SettingsAll struct {
|
||||||
|
// TODO(a.garipov): Add more as we go.
|
||||||
|
|
||||||
|
DNS *HTTPAPIDNSSettings `json:"dns"`
|
||||||
|
HTTP *HTTPAPIHTTPSettings `json:"http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
|
||||||
|
// API.
|
||||||
|
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsSvc := svc.confMgr.DNS()
|
||||||
|
dnsConf := dnsSvc.Config()
|
||||||
|
|
||||||
|
webSvc := svc.confMgr.Web()
|
||||||
|
httpConf := webSvc.Config()
|
||||||
|
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
|
||||||
|
DNS: &HTTPAPIDNSSettings{
|
||||||
|
Addresses: dnsConf.Addresses,
|
||||||
|
BootstrapServers: dnsConf.BootstrapServers,
|
||||||
|
UpstreamServers: dnsConf.UpstreamServers,
|
||||||
|
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
|
||||||
|
},
|
||||||
|
HTTP: &HTTPAPIHTTPSettings{
|
||||||
|
Addresses: httpConf.Addresses,
|
||||||
|
SecureAddresses: httpConf.SecureAddresses,
|
||||||
|
Timeout: JSONDuration(httpConf.Timeout),
|
||||||
|
ForceHTTPS: httpConf.ForceHTTPS,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestService_HandleGetSettingsAll(t *testing.T) {
|
||||||
|
// TODO(a.garipov): Add all currently supported parameters.
|
||||||
|
|
||||||
|
wantDNS := &websvc.HTTPAPIDNSSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
|
||||||
|
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
|
||||||
|
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
|
||||||
|
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
|
||||||
|
}
|
||||||
|
|
||||||
|
wantWeb := &websvc.HTTPAPIHTTPSettings{
|
||||||
|
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
|
||||||
|
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
|
||||||
|
Timeout: websvc.JSONDuration(5 * time.Second),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr := newConfigManager()
|
||||||
|
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
|
||||||
|
c, err := dnssvc.New(&dnssvc.Config{
|
||||||
|
Addresses: wantDNS.Addresses,
|
||||||
|
UpstreamServers: wantDNS.UpstreamServers,
|
||||||
|
BootstrapServers: wantDNS.BootstrapServers,
|
||||||
|
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
|
||||||
|
return websvc.New(&websvc.Config{
|
||||||
|
TLS: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{{}},
|
||||||
|
},
|
||||||
|
Addresses: wantWeb.Addresses,
|
||||||
|
SecureAddresses: wantWeb.SecureAddresses,
|
||||||
|
Timeout: time.Duration(wantWeb.Timeout),
|
||||||
|
ForceHTTPS: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
|
u := &url.URL{
|
||||||
|
Scheme: "http",
|
||||||
|
Host: addr.String(),
|
||||||
|
Path: websvc.PathV1SettingsAll,
|
||||||
|
}
|
||||||
|
|
||||||
|
body := httpGet(t, u, http.StatusOK)
|
||||||
|
resp := &websvc.RespGetV1SettingsAll{}
|
||||||
|
err := json.Unmarshal(body, resp)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, wantDNS, resp.DNS)
|
||||||
|
assert.Equal(t, wantWeb, resp.HTTP)
|
||||||
|
}
|
|
@ -16,20 +16,20 @@ type RespGetV1SystemInfo struct {
|
||||||
Channel string `json:"channel"`
|
Channel string `json:"channel"`
|
||||||
OS string `json:"os"`
|
OS string `json:"os"`
|
||||||
NewVersion string `json:"new_version,omitempty"`
|
NewVersion string `json:"new_version,omitempty"`
|
||||||
Start jsonTime `json:"start"`
|
Start JSONTime `json:"start"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
// handleGetV1SystemInfo is the handler for the GET /api/v1/system/info HTTP
|
||||||
// API.
|
// API.
|
||||||
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
func (svc *Service) handleGetV1SystemInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
writeJSONResponse(w, r, &RespGetV1SystemInfo{
|
writeJSONOKResponse(w, r, &RespGetV1SystemInfo{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Channel: version.Channel(),
|
Channel: version.Channel(),
|
||||||
OS: runtime.GOOS,
|
OS: runtime.GOOS,
|
||||||
// TODO(a.garipov): Fill this when we have an updater.
|
// TODO(a.garipov): Fill this when we have an updater.
|
||||||
NewVersion: "",
|
NewVersion: "",
|
||||||
Start: jsonTime(svc.start),
|
Start: JSONTime(svc.start),
|
||||||
Version: version.Version(),
|
Version: version.Version(),
|
||||||
})
|
})
|
||||||
}
|
}
|
|
@ -8,16 +8,17 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/websvc"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
func TestService_handleGetV1SystemInfo(t *testing.T) {
|
||||||
_, addr := newTestServer(t)
|
confMgr := newConfigManager()
|
||||||
|
_, addr := newTestServer(t, confMgr)
|
||||||
u := &url.URL{
|
u := &url.URL{
|
||||||
Scheme: "http",
|
Scheme: "http",
|
||||||
Host: addr,
|
Host: addr.String(),
|
||||||
Path: websvc.PathV1SystemInfo,
|
Path: websvc.PathV1SystemInfo,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wait Listener
|
||||||
|
|
||||||
|
// waitListener is a wrapper around a listener that also calls wg.Done() on the
|
||||||
|
// first call to Accept. It is useful in situations where it is important to
|
||||||
|
// catch the precise moment of the first call to Accept, for example when
|
||||||
|
// starting an HTTP server.
|
||||||
|
//
|
||||||
|
// TODO(a.garipov): Move to aghnet?
|
||||||
|
type waitListener struct {
|
||||||
|
net.Listener
|
||||||
|
|
||||||
|
firstAcceptWG *sync.WaitGroup
|
||||||
|
firstAcceptOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// type check
|
||||||
|
var _ net.Listener = (*waitListener)(nil)
|
||||||
|
|
||||||
|
// Accept implements the [net.Listener] interface for *waitListener.
|
||||||
|
func (l *waitListener) Accept() (conn net.Conn, err error) {
|
||||||
|
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
|
||||||
|
|
||||||
|
return l.Listener.Accept()
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWaitListener_Accept(t *testing.T) {
|
||||||
|
// TODO(a.garipov): use atomic.Bool in Go 1.19.
|
||||||
|
var numAcceptCalls uint32
|
||||||
|
var l net.Listener = &aghtest.Listener{
|
||||||
|
OnAccept: func() (conn net.Conn, err error) {
|
||||||
|
atomic.AddUint32(&numAcceptCalls, 1)
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
OnAddr: func() (addr net.Addr) { panic("not implemented") },
|
||||||
|
OnClose: func() (err error) { panic("not implemented") },
|
||||||
|
}
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(1)
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
go aghchan.MustReceive(done, testTimeout)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var wrapper net.Listener = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = wrapper.Accept()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(done)
|
||||||
|
|
||||||
|
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
|
||||||
|
}
|
|
@ -1,4 +1,7 @@
|
||||||
// Package websvc contains the AdGuard Home web service.
|
// 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.
|
// TODO(a.garipov): Add tests.
|
||||||
package websvc
|
package websvc
|
||||||
|
@ -14,18 +17,35 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
|
||||||
|
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
|
||||||
"github.com/AdguardTeam/golibs/errors"
|
"github.com/AdguardTeam/golibs/errors"
|
||||||
"github.com/AdguardTeam/golibs/log"
|
"github.com/AdguardTeam/golibs/log"
|
||||||
httptreemux "github.com/dimfeld/httptreemux/v5"
|
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.
|
// Config is the AdGuard Home web service configuration structure.
|
||||||
type Config struct {
|
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,
|
// TLS is the optional TLS configuration. If TLS is not nil,
|
||||||
// SecureAddresses must not be empty.
|
// SecureAddresses must not be empty.
|
||||||
TLS *tls.Config
|
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 are the addresses on which to serve the plain HTTP API.
|
||||||
Addresses []netip.AddrPort
|
Addresses []netip.AddrPort
|
||||||
|
|
||||||
|
@ -33,40 +53,48 @@ type Config struct {
|
||||||
// SecureAddresses is not empty, TLS must not be nil.
|
// SecureAddresses is not empty, TLS must not be nil.
|
||||||
SecureAddresses []netip.AddrPort
|
SecureAddresses []netip.AddrPort
|
||||||
|
|
||||||
// Start is the time of start of AdGuard Home.
|
|
||||||
Start time.Time
|
|
||||||
|
|
||||||
// Timeout is the timeout for all server operations.
|
// Timeout is the timeout for all server operations.
|
||||||
Timeout time.Duration
|
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
|
// Service is the AdGuard Home web service. A nil *Service is a valid
|
||||||
// [agh.Service] that does nothing.
|
// [agh.Service] that does nothing.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
tls *tls.Config
|
confMgr ConfigManager
|
||||||
servers []*http.Server
|
tls *tls.Config
|
||||||
start time.Time
|
start time.Time
|
||||||
timeout time.Duration
|
servers []*http.Server
|
||||||
|
timeout time.Duration
|
||||||
|
forceHTTPS bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
// New returns a new properly initialized *Service. If c is nil, svc is a nil
|
||||||
// *Service that does nothing.
|
// *Service that does nothing. The fields of c must not be modified after
|
||||||
|
// calling New.
|
||||||
func New(c *Config) (svc *Service) {
|
func New(c *Config) (svc *Service) {
|
||||||
if c == nil {
|
if c == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
svc = &Service{
|
svc = &Service{
|
||||||
tls: c.TLS,
|
confMgr: c.ConfigManager,
|
||||||
start: c.Start,
|
tls: c.TLS,
|
||||||
timeout: c.Timeout,
|
start: c.Start,
|
||||||
|
timeout: c.Timeout,
|
||||||
|
forceHTTPS: c.ForceHTTPS,
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := newMux(svc)
|
mux := newMux(svc)
|
||||||
|
|
||||||
for _, a := range c.Addresses {
|
for _, a := range c.Addresses {
|
||||||
addr := a.String()
|
addr := a.String()
|
||||||
errLog := log.StdLog("websvc: http: "+addr, log.ERROR)
|
errLog := log.StdLog("websvc: plain http: "+addr, log.ERROR)
|
||||||
svc.servers = append(svc.servers, &http.Server{
|
svc.servers = append(svc.servers, &http.Server{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
|
@ -111,6 +139,21 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
path: PathHealthCheck,
|
path: PathHealthCheck,
|
||||||
isJSON: false,
|
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,
|
handler: svc.handleGetV1SystemInfo,
|
||||||
method: http.MethodGet,
|
method: http.MethodGet,
|
||||||
|
@ -119,29 +162,41 @@ func newMux(svc *Service) (mux *httptreemux.ContextMux) {
|
||||||
}}
|
}}
|
||||||
|
|
||||||
for _, r := range routes {
|
for _, r := range routes {
|
||||||
var h http.HandlerFunc
|
|
||||||
if r.isJSON {
|
if r.isJSON {
|
||||||
// TODO(a.garipov): Consider using httptreemux's MiddlewareFunc.
|
mux.Handle(r.method, r.path, jsonMw(r.handler))
|
||||||
h = jsonMw(r.handler)
|
|
||||||
} else {
|
} else {
|
||||||
h = r.handler
|
mux.Handle(r.method, r.path, r.handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux.Handle(r.method, r.path, h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
// Addrs returns all addresses on which this server serves the HTTP API. Addrs
|
// addrs returns all addresses on which this server serves the HTTP API. addrs
|
||||||
// must not be called until Start returns.
|
// must not be called simultaneously with Start. If svc was initialized with
|
||||||
func (svc *Service) Addrs() (addrs []string) {
|
// ":0" addresses, addrs will not return the actual bound ports until Start is
|
||||||
addrs = make([]string, 0, len(svc.servers))
|
// finished.
|
||||||
|
func (svc *Service) addrs() (addrs, secureAddrs []netip.AddrPort) {
|
||||||
for _, srv := range svc.servers {
|
for _, srv := range svc.servers {
|
||||||
addrs = append(addrs, srv.Addr)
|
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
|
return addrs, secureAddrs
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
// handleGetHealthCheck is the handler for the GET /health-check HTTP API.
|
||||||
|
@ -149,9 +204,6 @@ func (svc *Service) handleGetHealthCheck(w http.ResponseWriter, _ *http.Request)
|
||||||
_, _ = io.WriteString(w, "OK")
|
_, _ = io.WriteString(w, "OK")
|
||||||
}
|
}
|
||||||
|
|
||||||
// unit is a convenient alias for struct{}.
|
|
||||||
type unit = struct{}
|
|
||||||
|
|
||||||
// type check
|
// type check
|
||||||
var _ agh.Service = (*Service)(nil)
|
var _ agh.Service = (*Service)(nil)
|
||||||
|
|
||||||
|
@ -163,11 +215,9 @@ func (svc *Service) Start() (err error) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
srvs := svc.servers
|
|
||||||
|
|
||||||
wg := &sync.WaitGroup{}
|
wg := &sync.WaitGroup{}
|
||||||
wg.Add(len(srvs))
|
wg.Add(len(svc.servers))
|
||||||
for _, srv := range srvs {
|
for _, srv := range svc.servers {
|
||||||
go serve(srv, wg)
|
go serve(srv, wg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,11 +231,14 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||||
addr := srv.Addr
|
addr := srv.Addr
|
||||||
defer log.OnPanic(addr)
|
defer log.OnPanic(addr)
|
||||||
|
|
||||||
|
var proto string
|
||||||
var l net.Listener
|
var l net.Listener
|
||||||
var err error
|
var err error
|
||||||
if srv.TLSConfig == nil {
|
if srv.TLSConfig == nil {
|
||||||
|
proto = "http"
|
||||||
l, err = net.Listen("tcp", addr)
|
l, err = net.Listen("tcp", addr)
|
||||||
} else {
|
} else {
|
||||||
|
proto = "https"
|
||||||
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
l, err = tls.Listen("tcp", addr, srv.TLSConfig)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -196,8 +249,12 @@ func serve(srv *http.Server, wg *sync.WaitGroup) {
|
||||||
// would mean that a random available port was automatically chosen.
|
// would mean that a random available port was automatically chosen.
|
||||||
srv.Addr = l.Addr().String()
|
srv.Addr = l.Addr().String()
|
||||||
|
|
||||||
log.Info("websvc: starting srv http://%s", srv.Addr)
|
log.Info("websvc: starting srv %s://%s", proto, srv.Addr)
|
||||||
wg.Done()
|
|
||||||
|
l = &waitListener{
|
||||||
|
Listener: l,
|
||||||
|
firstAcceptWG: wg,
|
||||||
|
}
|
||||||
|
|
||||||
err = srv.Serve(l)
|
err = srv.Serve(l)
|
||||||
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
@ -221,8 +278,28 @@ func (svc *Service) Shutdown(ctx context.Context) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errors.List("shutting down")
|
return errors.List("shutting down", errs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
package websvc
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// testTimeout is the common timeout for tests.
|
||||||
|
const testTimeout = 1 * time.Second
|
|
@ -0,0 +1,188 @@
|
||||||
|
package websvc_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/netip"
|
||||||
|
"net/url"
|
||||||
|
"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/AdguardTeam/golibs/testutil"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
aghtest.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)
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package querylog
|
package querylog
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -48,24 +47,7 @@ func (l *queryLog) handleQueryLog(w http.ResponseWriter, r *http.Request) {
|
||||||
// convert log entries to JSON
|
// convert log entries to JSON
|
||||||
data := l.entriesToJSON(entries, oldest)
|
data := l.entriesToJSON(entries, oldest)
|
||||||
|
|
||||||
jsonVal, err := json.Marshal(data)
|
_ = aghhttp.WriteJSONResponse(w, r, data)
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(
|
|
||||||
r,
|
|
||||||
w,
|
|
||||||
http.StatusInternalServerError,
|
|
||||||
"Couldn't marshal data into json: %s",
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_, err = w.Write(jsonVal)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "Unable to write response json: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||||
|
@ -74,23 +56,13 @@ func (l *queryLog) handleQueryLogClear(_ http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
||||||
// Get configuration
|
// Get configuration
|
||||||
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
func (l *queryLog) handleQueryLogInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := qlogConfig{}
|
resp := qlogConfig{
|
||||||
resp.Enabled = l.conf.Enabled
|
Enabled: l.conf.Enabled,
|
||||||
resp.Interval = l.conf.RotationIvl.Hours() / 24
|
Interval: l.conf.RotationIvl.Hours() / 24,
|
||||||
resp.AnonymizeClientIP = l.conf.AnonymizeClientIP
|
AnonymizeClientIP: l.conf.AnonymizeClientIP,
|
||||||
|
|
||||||
jsonVal, err := json.Marshal(resp)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
_, err = w.Write(jsonVal)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "http write: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
// AnonymizeIP masks ip to anonymize the client if the ip is a valid one.
|
||||||
|
|
|
@ -55,12 +55,7 @@ func (s *StatsCtx) handleStats(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// configResp is the response to the GET /control/stats_info.
|
// configResp is the response to the GET /control/stats_info.
|
||||||
|
@ -71,13 +66,7 @@ type configResp struct {
|
||||||
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
|
// handleStatsInfo handles requests to the GET /control/stats_info endpoint.
|
||||||
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
func (s *StatsCtx) handleStatsInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
|
resp := configResp{IntervalDays: atomic.LoadUint32(&s.limitHours) / 24}
|
||||||
|
_ = aghhttp.WriteJSONResponse(w, r, resp)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
err := json.NewEncoder(w).Encode(resp)
|
|
||||||
if err != nil {
|
|
||||||
aghhttp.Error(r, w, http.StatusInternalServerError, "json encode: %s", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleStatsConfig handles requests to the POST /control/stats_config
|
// handleStatsConfig handles requests to the POST /control/stats_config
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
// Package agh contains common entities and interfaces of AdGuard Home.
|
|
||||||
//
|
|
||||||
// TODO(a.garipov): Move to the upper-level internal/.
|
|
||||||
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.
|
|
||||||
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 }
|
|
|
@ -1,70 +0,0 @@
|
||||||
package cmd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
|
|
||||||
"github.com/AdguardTeam/AdGuardHome/internal/v1/agh"
|
|
||||||
"github.com/AdguardTeam/golibs/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// signalHandler processes incoming signals and shuts services down.
|
|
||||||
type signalHandler struct {
|
|
||||||
signal chan os.Signal
|
|
||||||
|
|
||||||
// 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.IsShutdownSignal(sig) {
|
|
||||||
h.shutdown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exit status constants.
|
|
||||||
const (
|
|
||||||
statusSuccess = 0
|
|
||||||
statusError = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
// shutdown gracefully shuts down all services.
|
|
||||||
func (h *signalHandler) shutdown() {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info("sighdlr: shutting down adguard home")
|
|
||||||
|
|
||||||
os.Exit(status)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newSignalHandler returns a new signalHandler that shuts down svcs.
|
|
||||||
func newSignalHandler(svcs ...agh.Service) (h *signalHandler) {
|
|
||||||
h = &signalHandler{
|
|
||||||
signal: make(chan os.Signal, 1),
|
|
||||||
services: svcs,
|
|
||||||
}
|
|
||||||
|
|
||||||
aghos.NotifyShutdownSignal(h.signal)
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue