Merge branch 'master' into 4990-custom-ciphers

This commit is contained in:
Ainar Garipov 2022-10-14 19:41:43 +03:00
commit a736f67205
109 changed files with 3936 additions and 2010 deletions

View File

@ -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':

View File

@ -1,7 +1,7 @@
'name': 'lint' 'name': 'lint'
'env': 'env':
'GO_VERSION': '1.18.6' 'GO_VERSION': '1.18.7'
'on': 'on':
'push': 'push':

View File

@ -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

View File

@ -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)'\

517
README.md
View File

@ -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)
* [Alternative methods](#alternative-methods)
* [Guides](#guides)
* [API](#api)
* [Comparing AdGuard Home to other solutions](#comparison)
* [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns) * [How is this different from public AdGuard DNS servers?](#comparison-adguard-dns)
* [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole) * [How does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
* [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock) * [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
* [How to build from source](#how-to-build) * [Known limitations](#comparison-limitations)
* [Contributing](#contributing) * [How to build from source](#how-to-build)
* [Prerequisites](#prerequisites)
* [Building](#building)
* [Contributing](#contributing)
* [Test unstable versions](#test-unstable-versions) * [Test unstable versions](#test-unstable-versions)
* [Reporting issues](#reporting-issues) * [Reporting issues](#reporting-issues)
* [Help with translations](#translate) * [Help with translations](#translate)
* [Other](#help-other) * [Other](#help-other)
* [Projects that use AdGuard Home](#uses) * [Projects that use AdGuard Home](#uses)
* [Acknowledgments](#acknowledgments) * [Acknowledgments](#acknowledgments)
* [Privacy](#privacy) * [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&nbsp;Home | Pi-Hole | | Feature | AdGuard&nbsp;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,141 +291,190 @@ 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:
@ -385,22 +486,28 @@ This software wouldn't have been possible without:
* [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:
* And many more Node.js packages.
* [React.js](https://reactjs.org) * [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler) * [Tabler](https://github.com/tabler/tabler)
* And many more node.js packages.
* [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

View File

@ -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'

View File

@ -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':

View File

@ -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)",

View File

@ -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}
> >

View File

@ -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 {

View File

@ -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)}

View File

@ -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;

View File

@ -47,17 +47,20 @@ const processContent = (data) => Object.entries(data)
keyClass = ''; keyClass = '';
} }
return isHidden ? null : <div key={key}> return isHidden ? null : (
<div className="grid__row" key={key}>
<div <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>
<div className={`value__${key} text-pre text-truncate`}> <div className={`value__${key} text-pre text-truncate`}>
<Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans> <Trans>{(isTitle || isButton || isBoolean) ? '' : value || '—'}</Trans>
</div> </div>
</div>; </div>
);
}); });
const Logs = () => { const Logs = () => {

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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
}

View File

@ -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 {

View File

@ -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.

View File

@ -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)

View File

@ -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.

View File

@ -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
select {
case upd, ok = <-hc.Upd():
require.True(t, ok) require.True(t, ok)
require.NotNil(t, upd) 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())

View File

@ -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 {
netIface := &NetInterface{
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. // Discard interfaces with no addresses.
if len(netIface.Addresses) != 0 { nifaces = append(nifaces, niface)
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
} }

View File

@ -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()
} }

View File

@ -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 {
pref netip.Prefix
want netip.Addr
name string name string
subnet *net.IPNet
want net.IP
}{{ }{{
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,

View File

@ -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
} }

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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)
})
}
}

14
internal/aghtls/root.go Normal file
View File

@ -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()
}

View File

@ -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
}

View File

@ -0,0 +1,9 @@
//go:build !linux
package aghtls
import "crypto/x509"
func rootCAs() (roots *x509.CertPool) {
return nil
}

View File

@ -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,

View File

@ -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
} }

View File

@ -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 {

View File

@ -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,

View File

@ -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,10 +18,10 @@ 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"`
} }
@ -39,7 +40,7 @@ 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"`
} }
@ -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 {

View File

@ -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

View File

@ -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") }()

View File

@ -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.

View File

@ -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,
}, },
} }

View File

@ -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 {

View File

@ -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())
}) })

View File

@ -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

View File

@ -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
}

View File

@ -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,23 +189,40 @@ 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
serverName: tc.cliSrvName, httpReq *http.Request
} )
switch tc.proto {
case proxy.ProtoHTTPS:
u := &url.URL{
Path: "/dns-query",
} }
var qconn quic.Connection connState := &tls.ConnectionState{
if tc.proto == proxy.ProtoQUIC { ServerName: tc.cliSrvName,
}
httpReq = &http.Request{
URL: u,
TLS: connState,
}
case proxy.ProtoQUIC:
qconn = testQUICConnection{ qconn = testQUICConnection{
serverName: tc.cliSrvName, serverName: tc.cliSrvName,
} }
case proxy.ProtoTLS:
conn = testTLSConn{
serverName: tc.cliSrvName,
}
} }
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{

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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)
} }

View File

@ -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{

View File

@ -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()

View File

@ -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

View File

@ -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"),
}, },
} }

View File

@ -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:

View File

@ -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
} }

View File

@ -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,7 +64,7 @@ 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"`
} }
@ -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,7 +304,7 @@ 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"`
} }
@ -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,7 +518,7 @@ 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"`
} }
@ -590,7 +590,7 @@ 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"`
} }

View File

@ -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")
} }

View 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 {
log.Error(err.Error())
printHelp(os.Args[0])
if err != nil {
log.Error(err.Error())
_ = 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)
} }
} }

View File

@ -0,0 +1,12 @@
package home
import (
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
)
func TestMain(m *testing.M) {
aghtest.DiscardLogOutput(m)
initCmdLineOpts()
}

View File

@ -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)

View File

@ -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)
// 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
}
// cmdLineOpts are all command-line options of AdGuard Home.
var cmdLineOpts = []cmdLineOpt{{
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 o, err
},
updateNoValue: nil,
effect: nil,
serialize: func(o options) (val string, ok bool) {
if !o.bindHost.IsValid() {
return "", false
} }
return []string{ip.String()} return o.bindHost.String(), true
} },
description: "Host address to bind HTTP server on.",
func stringSliceOrNil(s string) []string { longName: "host",
if s == "" { shortName: "h",
return nil }, {
} updateWithValue: func(o options, v string) (options, error) {
return []string{s}
}
func intSliceOrNil(i int) []string {
if i == 0 {
return nil
}
return []string{strconv.Itoa(i)}
}
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{
"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: "", 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{
description: "Use local frontend directories.",
longName: "local-frontend",
shortName: "", 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",
func init() { shortName: "",
args = []arg{ }, {
configArg, updateWithValue: nil,
workDirArg, updateNoValue: func(o options) (options, error) { o.verbose = true; return o, nil },
hostArg, effect: nil,
portArg, serialize: func(o options) (val string, ok bool) { return "", o.verbose },
serviceArg, description: "Enable verbose output.",
logfileArg, longName: "verbose",
pidfileArg, shortName: "v",
checkConfigArg, }, {
noCheckUpdateArg, updateWithValue: nil,
disableMemoryOptimizationArg, updateNoValue: func(o options) (options, error) { o.glinetMode = true; return o, nil },
noEtcHostsArg, effect: nil,
localFrontendArg, serialize: func(o options) (val string, ok bool) { return "", o.glinetMode },
verboseArg, description: "Run in GL-Inet compatibility mode.",
glinetArg, longName: "glinet",
versionArg, shortName: "",
helpArg, }, {
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 getUsageLines(exec string, args []arg) []string { os.Exit(0)
usage := []string{
"Usage:", return nil
"", }, nil
fmt.Sprintf("%s [options]", exec), },
"", serialize: func(o options) (val string, ok bool) { return "", false },
"Options:", description: "Show the version and exit. Show more detailed version description with -v.",
} longName: "version",
for _, arg := range args { shortName: "",
}}
// 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)
if !ok {
continue
}
func parse(exec string, ss []string) (o options, f effect, err error) { if opt.shortName != "" {
for i := 0; i < len(ss); i++ { args = append(args, "-"+opt.shortName)
v := ss[i] } else {
knownParam := false args = append(args, "--"+opt.longName)
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 val != "" {
if err != nil { args = append(args, val)
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)
} }
} }
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
} }

View File

@ -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)
}) })
} }
} }

View File

@ -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)

View File

@ -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
// certLastMod is the last modification time of the certificate file.
certLastMod time.Time
// TLSMod - TLS module object
type TLSMod struct {
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{
Enabled: conf.Enabled,
ServerName: conf.ServerName,
PortHTTPS: conf.PortHTTPS,
PortDNSOverTLS: conf.PortDNSOverTLS,
PortDNSOverQUIC: conf.PortDNSOverQUIC,
AllowUnencryptedDoH: conf.AllowUnencryptedDoH,
}}
} }
t.setCertFileTime()
if m.conf.Enabled {
err = m.load()
if err != nil {
return nil, err
} }
return t
m.setCertFileTime()
}
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)
for _, cert := range certs {
parsed, err := x509.ParseCertificate(cert.Bytes)
if err != nil { if err != nil {
data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err) return 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.Printf("got an intermediate cert") log.Info("tls: got an intermediate cert")
pool.AddCert(cert) pool.AddCert(cert)
} }
opts.Intermediates = pool opts.Intermediates = pool
}
// TODO: save it as a warning rather than error it out -- shouldn't be a big problem
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,
pkey []byte,
serverName string,
) (err error) {
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:]
}
}()
// check only public certificate separately from the key // Check only the public certificate separately from the key.
if certChain != "" { if len(certChain) > 0 {
if verifyCertChain(&data, certChain, serverName) != nil { err = validateCertChain(status, certChain, serverName)
return data
}
}
// validate private key (right now the only validation possible is just parsing it)
if pkey != "" {
if validatePkey(&data, pkey) != nil {
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
} }

View File

@ -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)
}) })
} }

View File

@ -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

63
internal/next/agh/agh.go Normal file
View File

@ -0,0 +1,63 @@
// Package agh contains common entities and interfaces of AdGuard Home.
package agh
import "context"
// Service is the interface for API servers.
//
// TODO(a.garipov): Consider adding a context to Start.
//
// TODO(a.garipov): Consider adding a Wait method or making an extension
// interface for that.
type Service interface {
// Start starts the service. It does not block.
Start() (err error)
// Shutdown gracefully stops the service. ctx is used to determine
// a timeout before trying to stop the service less gracefully.
Shutdown(ctx context.Context) (err error)
}
// type check
var _ Service = EmptyService{}
// EmptyService is a [Service] that does nothing.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyService struct{}
// Start implements the [Service] interface for EmptyService.
func (EmptyService) Start() (err error) { return nil }
// Shutdown implements the [Service] interface for EmptyService.
func (EmptyService) Shutdown(_ context.Context) (err error) { return nil }
// ServiceWithConfig is an extension of the [Service] interface for services
// that can return their configuration.
//
// TODO(a.garipov): Consider removing this generic interface if we figure out
// how to make it testable in a better way.
type ServiceWithConfig[ConfigType any] interface {
Service
Config() (c ConfigType)
}
// type check
var _ ServiceWithConfig[struct{}] = (*EmptyServiceWithConfig[struct{}])(nil)
// EmptyServiceWithConfig is a ServiceWithConfig that does nothing. Its Config
// method returns Conf.
//
// TODO(a.garipov): Remove if unnecessary.
type EmptyServiceWithConfig[ConfigType any] struct {
EmptyService
Conf ConfigType
}
// Config implements the [ServiceWithConfig] interface for
// *EmptyServiceWithConfig.
func (s *EmptyServiceWithConfig[ConfigType]) Config() (conf ConfigType) {
return s.Conf
}

View File

@ -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()

118
internal/next/cmd/signal.go Normal file
View File

@ -0,0 +1,118 @@
package cmd
import (
"os"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/configmgr"
"github.com/AdguardTeam/golibs/log"
)
// signalHandler processes incoming signals and shuts services down.
type signalHandler struct {
// signal is the channel to which OS signals are sent.
signal chan os.Signal
// confFile is the path to the configuration file.
confFile string
// start is the time at which AdGuard Home has been started.
start time.Time
// services are the services that are shut down before application exiting.
services []agh.Service
}
// handle processes OS signals.
func (h *signalHandler) handle() {
defer log.OnPanic("signalHandler.handle")
for sig := range h.signal {
log.Info("sighdlr: received signal %q", sig)
if aghos.IsReconfigureSignal(sig) {
h.reconfigure()
} else if aghos.IsShutdownSignal(sig) {
status := h.shutdown()
log.Info("sighdlr: exiting with status %d", status)
os.Exit(status)
}
}
}
// reconfigure rereads the configuration file and updates and restarts services.
func (h *signalHandler) reconfigure() {
log.Info("sighdlr: reconfiguring adguard home")
status := h.shutdown()
if status != statusSuccess {
log.Info("sighdlr: reconfiruging: exiting with status %d", status)
os.Exit(status)
}
// TODO(a.garipov): This is a very rough way to do it. Some services can be
// reconfigured without the full shutdown, and the error handling is
// currently not the best.
confMgr, err := configmgr.New(h.confFile, h.start)
fatalOnError(err)
web := confMgr.Web()
err = web.Start()
fatalOnError(err)
dns := confMgr.DNS()
err = dns.Start()
fatalOnError(err)
h.services = []agh.Service{
dns,
web,
}
log.Info("sighdlr: successfully reconfigured adguard home")
}
// Exit status constants.
const (
statusSuccess = 0
statusError = 1
)
// shutdown gracefully shuts down all services.
func (h *signalHandler) shutdown() (status int) {
ctx, cancel := ctxWithDefaultTimeout()
defer cancel()
status = statusSuccess
log.Info("sighdlr: shutting down services")
for i, service := range h.services {
err := service.Shutdown(ctx)
if err != nil {
log.Error("sighdlr: shutting down service at index %d: %s", i, err)
status = statusError
}
}
return status
}
// newSignalHandler returns a new signalHandler that shuts down svcs.
func newSignalHandler(confFile string, start time.Time, svcs ...agh.Service) (h *signalHandler) {
h = &signalHandler{
signal: make(chan os.Signal, 1),
confFile: confFile,
start: start,
services: svcs,
}
aghos.NotifyShutdownSignal(h.signal)
aghos.NotifyReconfigureSignal(h.signal)
return h
}

View File

@ -0,0 +1,40 @@
package configmgr
import (
"net/netip"
"github.com/AdguardTeam/golibs/timeutil"
)
// Configuration Structures
// config is the top-level on-disk configuration structure.
type config struct {
DNS *dnsConfig `yaml:"dns"`
HTTP *httpConfig `yaml:"http"`
// TODO(a.garipov): Use.
SchemaVersion int `yaml:"schema_version"`
// TODO(a.garipov): Use.
DebugPprof bool `yaml:"debug_pprof"`
Verbose bool `yaml:"verbose"`
}
// dnsConfig is the on-disk DNS configuration.
//
// TODO(a.garipov): Validate.
type dnsConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
BootstrapDNS []string `yaml:"bootstrap_dns"`
UpstreamDNS []string `yaml:"upstream_dns"`
UpstreamTimeout timeutil.Duration `yaml:"upstream_timeout"`
}
// httpConfig is the on-disk web API configuration.
//
// TODO(a.garipov): Validate.
type httpConfig struct {
Addresses []netip.AddrPort `yaml:"addresses"`
SecureAddresses []netip.AddrPort `yaml:"secure_addresses"`
Timeout timeutil.Duration `yaml:"timeout"`
ForceHTTPS bool `yaml:"force_https"`
}

View File

@ -0,0 +1,205 @@
// Package configmgr defines the AdGuard Home on-disk configuration entities and
// configuration manager.
package configmgr
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
"gopkg.in/yaml.v3"
)
// Configuration Manager
// Manager handles full and partial changes in the configuration, persisting
// them to disk if necessary.
type Manager struct {
// updMu makes sure that at most one reconfiguration is performed at a time.
// updMu protects all fields below.
updMu *sync.RWMutex
// dns is the DNS service.
dns *dnssvc.Service
// Web is the Web API service.
web *websvc.Service
// current is the current configuration.
current *config
// fileName is the name of the configuration file.
fileName string
}
// New creates a new *Manager that persists changes to the file pointed to by
// fileName. It reads the configuration file and populates the service fields.
// start is the startup time of AdGuard Home.
func New(fileName string, start time.Time) (m *Manager, err error) {
defer func() { err = errors.Annotate(err, "reading config") }()
conf := &config{}
f, err := os.Open(fileName)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
defer func() { err = errors.WithDeferred(err, f.Close()) }()
err = yaml.NewDecoder(f).Decode(conf)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
// TODO(a.garipov): Move into a separate function and add other logging
// settings.
if conf.Verbose {
log.SetLevel(log.DEBUG)
}
// TODO(a.garipov): Validate the configuration structure. Return an error
// if it's incorrect.
m = &Manager{
updMu: &sync.RWMutex{},
current: conf,
fileName: fileName,
}
// TODO(a.garipov): Get the context with the timeout from the arguments?
const assemblyTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), assemblyTimeout)
defer cancel()
err = m.assemble(ctx, conf, start)
if err != nil {
// Don't wrap the error, because it's informative enough as is.
return nil, err
}
return m, nil
}
// assemble creates all services and puts them into the corresponding fields.
// The fields of conf must not be modified after calling assemble.
func (m *Manager) assemble(ctx context.Context, conf *config, start time.Time) (err error) {
dnsConf := &dnssvc.Config{
Addresses: conf.DNS.Addresses,
BootstrapServers: conf.DNS.BootstrapDNS,
UpstreamServers: conf.DNS.UpstreamDNS,
UpstreamTimeout: conf.DNS.UpstreamTimeout.Duration,
}
err = m.updateDNS(ctx, dnsConf)
if err != nil {
return fmt.Errorf("assembling dnssvc: %w", err)
}
webSvcConf := &websvc.Config{
ConfigManager: m,
// TODO(a.garipov): Fill from config file.
TLS: nil,
Start: start,
Addresses: conf.HTTP.Addresses,
SecureAddresses: conf.HTTP.SecureAddresses,
Timeout: conf.HTTP.Timeout.Duration,
ForceHTTPS: conf.HTTP.ForceHTTPS,
}
err = m.updateWeb(ctx, webSvcConf)
if err != nil {
return fmt.Errorf("assembling websvc: %w", err)
}
return nil
}
// DNS returns the current DNS service. It is safe for concurrent use.
func (m *Manager) DNS() (dns agh.ServiceWithConfig[*dnssvc.Config]) {
m.updMu.RLock()
defer m.updMu.RUnlock()
return m.dns
}
// UpdateDNS implements the [websvc.ConfigManager] interface for *Manager. The
// fields of c must not be modified after calling UpdateDNS.
func (m *Manager) UpdateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
m.updMu.Lock()
defer m.updMu.Unlock()
// TODO(a.garipov): Update and write the configuration file. Return an
// error if something went wrong.
err = m.updateDNS(ctx, c)
if err != nil {
return fmt.Errorf("reassembling dnssvc: %w", err)
}
return nil
}
// updateDNS recreates the DNS service. m.updMu is expected to be locked.
func (m *Manager) updateDNS(ctx context.Context, c *dnssvc.Config) (err error) {
if prev := m.dns; prev != nil {
err = prev.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutting down dns svc: %w", err)
}
}
svc, err := dnssvc.New(c)
if err != nil {
return fmt.Errorf("creating dns svc: %w", err)
}
m.dns = svc
return nil
}
// Web returns the current web service. It is safe for concurrent use.
func (m *Manager) Web() (web agh.ServiceWithConfig[*websvc.Config]) {
m.updMu.RLock()
defer m.updMu.RUnlock()
return m.web
}
// UpdateWeb implements the [websvc.ConfigManager] interface for *Manager. The
// fields of c must not be modified after calling UpdateWeb.
func (m *Manager) UpdateWeb(ctx context.Context, c *websvc.Config) (err error) {
m.updMu.Lock()
defer m.updMu.Unlock()
// TODO(a.garipov): Update and write the configuration file. Return an
// error if something went wrong.
err = m.updateWeb(ctx, c)
if err != nil {
return fmt.Errorf("reassembling websvc: %w", err)
}
return nil
}
// updateWeb recreates the web service. m.upd is expected to be locked.
func (m *Manager) updateWeb(ctx context.Context, c *websvc.Config) (err error) {
if prev := m.web; prev != nil {
err = prev.Shutdown(ctx)
if err != nil {
return fmt.Errorf("shutting down web svc: %w", err)
}
}
m.web = websvc.New(c)
return nil
}

View File

@ -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,14 +193,28 @@ 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?
var addrs []netip.AddrPort
if atomic.LoadUint64(&svc.running) == 1 {
udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP) udpAddrs := svc.proxy.Addrs(proxy.ProtoUDP)
addrs := make([]netip.AddrPort, len(udpAddrs)) addrs = make([]netip.AddrPort, len(udpAddrs))
for i, a := range udpAddrs { for i, a := range udpAddrs {
addrs[i] = a.(*net.UDPAddr).AddrPort() 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{
Addresses: addrs, Addresses: addrs,

View File

@ -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"

View File

@ -0,0 +1,84 @@
package websvc
import (
"encoding/json"
"fmt"
"net/http"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
)
// DNS Settings Handlers
// ReqPatchSettingsDNS describes the request to the PATCH /api/v1/settings/dns
// HTTP API.
type ReqPatchSettingsDNS struct {
// TODO(a.garipov): Add more as we go.
Addresses []netip.AddrPort `json:"addresses"`
BootstrapServers []string `json:"bootstrap_servers"`
UpstreamServers []string `json:"upstream_servers"`
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
}
// HTTPAPIDNSSettings are the DNS settings as used by the HTTP API. See the
// DnsSettings object in the OpenAPI specification.
type HTTPAPIDNSSettings struct {
// TODO(a.garipov): Add more as we go.
Addresses []netip.AddrPort `json:"addresses"`
BootstrapServers []string `json:"bootstrap_servers"`
UpstreamServers []string `json:"upstream_servers"`
UpstreamTimeout JSONDuration `json:"upstream_timeout"`
}
// handlePatchSettingsDNS is the handler for the PATCH /api/v1/settings/dns HTTP
// API.
func (svc *Service) handlePatchSettingsDNS(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsDNS{
Addresses: []netip.AddrPort{},
BootstrapServers: []string{},
UpstreamServers: []string{},
}
// TODO(a.garipov): Validate nulls and proper JSON patch.
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
return
}
newConf := &dnssvc.Config{
Addresses: req.Addresses,
BootstrapServers: req.BootstrapServers,
UpstreamServers: req.UpstreamServers,
UpstreamTimeout: time.Duration(req.UpstreamTimeout),
}
ctx := r.Context()
err = svc.confMgr.UpdateDNS(ctx, newConf)
if err != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", err))
return
}
newSvc := svc.confMgr.DNS()
err = newSvc.Start()
if err != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("starting new service: %w", err))
return
}
writeJSONOKResponse(w, r, &HTTPAPIDNSSettings{
Addresses: newConf.Addresses,
BootstrapServers: newConf.BootstrapServers,
UpstreamServers: newConf.UpstreamServers,
UpstreamTimeout: JSONDuration(newConf.UpstreamTimeout),
})
}

View File

@ -0,0 +1,69 @@
package websvc_test
import (
"context"
"encoding/json"
"net/http"
"net/netip"
"net/url"
"sync/atomic"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService_HandlePatchSettingsDNS(t *testing.T) {
wantDNS := &websvc.HTTPAPIDNSSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:53")},
BootstrapServers: []string{"1.0.0.1"},
UpstreamServers: []string{"1.1.1.1"},
UpstreamTimeout: websvc.JSONDuration(2 * time.Second),
}
// TODO(a.garipov): Use [atomic.Bool] in Go 1.19.
var numStarted uint64
confMgr := newConfigManager()
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
return &aghtest.ServiceWithConfig[*dnssvc.Config]{
OnStart: func() (err error) {
atomic.AddUint64(&numStarted, 1)
return nil
},
OnShutdown: func(_ context.Context) (err error) { panic("not implemented") },
OnConfig: func() (c *dnssvc.Config) { panic("not implemented") },
}
}
confMgr.onUpdateDNS = func(ctx context.Context, c *dnssvc.Config) (err error) {
return nil
}
_, addr := newTestServer(t, confMgr)
u := &url.URL{
Scheme: "http",
Host: addr.String(),
Path: websvc.PathV1SettingsDNS,
}
req := jobj{
"addresses": wantDNS.Addresses,
"bootstrap_servers": wantDNS.BootstrapServers,
"upstream_servers": wantDNS.UpstreamServers,
"upstream_timeout": wantDNS.UpstreamTimeout,
}
respBody := httpPatch(t, u, req, http.StatusOK)
resp := &websvc.HTTPAPIDNSSettings{}
err := json.Unmarshal(respBody, resp)
require.NoError(t, err)
assert.Equal(t, uint64(1), numStarted)
assert.Equal(t, wantDNS, resp)
assert.Equal(t, wantDNS, resp)
}

View File

@ -0,0 +1,110 @@
package websvc
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/golibs/log"
)
// HTTP Settings Handlers
// ReqPatchSettingsHTTP describes the request to the PATCH /api/v1/settings/http
// HTTP API.
type ReqPatchSettingsHTTP struct {
// TODO(a.garipov): Add more as we go.
//
// TODO(a.garipov): Add wait time.
Addresses []netip.AddrPort `json:"addresses"`
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
Timeout JSONDuration `json:"timeout"`
}
// HTTPAPIHTTPSettings are the HTTP settings as used by the HTTP API. See the
// HttpSettings object in the OpenAPI specification.
type HTTPAPIHTTPSettings struct {
// TODO(a.garipov): Add more as we go.
Addresses []netip.AddrPort `json:"addresses"`
SecureAddresses []netip.AddrPort `json:"secure_addresses"`
Timeout JSONDuration `json:"timeout"`
ForceHTTPS bool `json:"force_https"`
}
// handlePatchSettingsHTTP is the handler for the PATCH /api/v1/settings/http
// HTTP API.
func (svc *Service) handlePatchSettingsHTTP(w http.ResponseWriter, r *http.Request) {
req := &ReqPatchSettingsHTTP{}
// TODO(a.garipov): Validate nulls and proper JSON patch.
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("decoding: %w", err))
return
}
newConf := &Config{
ConfigManager: svc.confMgr,
TLS: svc.tls,
Addresses: req.Addresses,
SecureAddresses: req.SecureAddresses,
Timeout: time.Duration(req.Timeout),
ForceHTTPS: svc.forceHTTPS,
}
writeJSONOKResponse(w, r, &HTTPAPIHTTPSettings{
Addresses: newConf.Addresses,
SecureAddresses: newConf.SecureAddresses,
Timeout: JSONDuration(newConf.Timeout),
ForceHTTPS: newConf.ForceHTTPS,
})
cancelUpd := func() {}
updCtx := context.Background()
ctx := r.Context()
if deadline, ok := ctx.Deadline(); ok {
updCtx, cancelUpd = context.WithDeadline(updCtx, deadline)
}
// Launch the new HTTP service in a separate goroutine to let this handler
// finish and thus, this server to shutdown.
go func() {
defer cancelUpd()
updErr := svc.confMgr.UpdateWeb(updCtx, newConf)
if updErr != nil {
writeJSONErrorResponse(w, r, fmt.Errorf("updating: %w", updErr))
return
}
// TODO(a.garipov): Consider better ways to do this.
const maxUpdDur = 10 * time.Second
updStart := time.Now()
var newSvc agh.ServiceWithConfig[*Config]
for newSvc = svc.confMgr.Web(); newSvc == svc; {
if time.Since(updStart) >= maxUpdDur {
log.Error("websvc: failed to update svc after %s", maxUpdDur)
return
}
log.Debug("websvc: waiting for new websvc to be configured")
time.Sleep(1 * time.Second)
}
updErr = newSvc.Start()
if updErr != nil {
log.Error("websvc: new svc failed to start with error: %s", updErr)
}
}()
}

View File

@ -0,0 +1,63 @@
package websvc_test
import (
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/netip"
"net/url"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService_HandlePatchSettingsHTTP(t *testing.T) {
wantWeb := &websvc.HTTPAPIHTTPSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:80")},
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.1.1:443")},
Timeout: websvc.JSONDuration(10 * time.Second),
ForceHTTPS: false,
}
confMgr := newConfigManager()
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{
TLS: &tls.Config{
Certificates: []tls.Certificate{{}},
},
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
Timeout: 5 * time.Second,
ForceHTTPS: true,
})
}
confMgr.onUpdateWeb = func(ctx context.Context, c *websvc.Config) (err error) {
return nil
}
_, addr := newTestServer(t, confMgr)
u := &url.URL{
Scheme: "http",
Host: addr.String(),
Path: websvc.PathV1SettingsHTTP,
}
req := jobj{
"addresses": wantWeb.Addresses,
"secure_addresses": wantWeb.SecureAddresses,
"timeout": wantWeb.Timeout,
"force_https": wantWeb.ForceHTTPS,
}
respBody := httpPatch(t, u, req, http.StatusOK)
resp := &websvc.HTTPAPIHTTPSettings{}
err := json.Unmarshal(respBody, resp)
require.NoError(t, err)
assert.Equal(t, wantWeb, resp)
}

View File

@ -0,0 +1,143 @@
package websvc
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/golibs/log"
)
// JSON Utilities
// nsecPerMsec is the number of nanoseconds in a millisecond.
const nsecPerMsec = float64(time.Millisecond / time.Nanosecond)
// JSONDuration is a time.Duration that can be decoded from JSON and encoded
// into JSON according to our API conventions.
type JSONDuration time.Duration
// type check
var _ json.Marshaler = JSONDuration(0)
// MarshalJSON implements the json.Marshaler interface for JSONDuration. err is
// always nil.
func (d JSONDuration) MarshalJSON() (b []byte, err error) {
msec := float64(time.Duration(d)) / nsecPerMsec
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
return b, nil
}
// type check
var _ json.Unmarshaler = (*JSONDuration)(nil)
// UnmarshalJSON implements the json.Marshaler interface for *JSONDuration.
func (d *JSONDuration) UnmarshalJSON(b []byte) (err error) {
if d == nil {
return fmt.Errorf("json duration is nil")
}
msec, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return fmt.Errorf("parsing json time: %w", err)
}
*d = JSONDuration(int64(msec * nsecPerMsec))
return nil
}
// JSONTime is a time.Time that can be decoded from JSON and encoded into JSON
// according to our API conventions.
type JSONTime time.Time
// type check
var _ json.Marshaler = JSONTime{}
// MarshalJSON implements the json.Marshaler interface for JSONTime. err is
// always nil.
func (t JSONTime) MarshalJSON() (b []byte, err error) {
msec := float64(time.Time(t).UnixNano()) / nsecPerMsec
b = strconv.AppendFloat(nil, msec, 'f', -1, 64)
return b, nil
}
// type check
var _ json.Unmarshaler = (*JSONTime)(nil)
// UnmarshalJSON implements the json.Marshaler interface for *JSONTime.
func (t *JSONTime) UnmarshalJSON(b []byte) (err error) {
if t == nil {
return fmt.Errorf("json time is nil")
}
msec, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return fmt.Errorf("parsing json time: %w", err)
}
*t = JSONTime(time.Unix(0, int64(msec*nsecPerMsec)).UTC())
return nil
}
// writeJSONOKResponse writes headers with the code 200 OK, encodes v into w,
// and logs any errors it encounters. r is used to get additional information
// from the request.
func writeJSONOKResponse(w http.ResponseWriter, r *http.Request, v any) {
writeJSONResponse(w, r, v, http.StatusOK)
}
// writeJSONResponse writes headers with code, encodes v into w, and logs any
// errors it encounters. r is used to get additional information from the
// request.
func writeJSONResponse(w http.ResponseWriter, r *http.Request, v any, code int) {
// TODO(a.garipov): Put some of these to a middleware.
h := w.Header()
h.Set(aghhttp.HdrNameContentType, aghhttp.HdrValApplicationJSON)
h.Set(aghhttp.HdrNameServer, aghhttp.UserAgent())
w.WriteHeader(code)
err := json.NewEncoder(w).Encode(v)
if err != nil {
log.Error("websvc: writing resp to %s %s: %s", r.Method, r.URL.Path, err)
}
}
// ErrorCode is the error code as used by the HTTP API. See the ErrorCode
// definition in the OpenAPI specification.
type ErrorCode string
// ErrorCode constants.
//
// TODO(a.garipov): Expand and document codes.
const (
// ErrorCodeTMP000 is the temporary error code used for all errors.
ErrorCodeTMP000 = ""
)
// HTTPAPIErrorResp is the error response as used by the HTTP API. See the
// BadRequestResp, InternalServerErrorResp, and similar objects in the OpenAPI
// specification.
type HTTPAPIErrorResp struct {
Code ErrorCode `json:"code"`
Msg string `json:"msg"`
}
// writeJSONErrorResponse encodes err as a JSON error into w, and logs any
// errors it encounters. r is used to get additional information from the
// request.
func writeJSONErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
log.Error("websvc: %s %s: %s", r.Method, r.URL.Path, err)
writeJSONResponse(w, r, &HTTPAPIErrorResp{
Code: ErrorCodeTMP000,
Msg: err.Error(),
}, http.StatusUnprocessableEntity)
}

View File

@ -0,0 +1,114 @@
package websvc_test
import (
"encoding/json"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testJSONTime is the JSON time for tests.
var testJSONTime = websvc.JSONTime(time.Unix(1_234_567_890, 123_456_000).UTC())
// testJSONTimeStr is the string with the JSON encoding of testJSONTime.
const testJSONTimeStr = "1234567890123.456"
func TestJSONTime_MarshalJSON(t *testing.T) {
testCases := []struct {
name string
wantErrMsg string
in websvc.JSONTime
want []byte
}{{
name: "unix_zero",
wantErrMsg: "",
in: websvc.JSONTime(time.Unix(0, 0)),
want: []byte("0"),
}, {
name: "empty",
wantErrMsg: "",
in: websvc.JSONTime{},
want: []byte("-6795364578871.345"),
}, {
name: "time",
wantErrMsg: "",
in: testJSONTime,
want: []byte(testJSONTimeStr),
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.in.MarshalJSON()
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.want, got)
})
}
t.Run("json", func(t *testing.T) {
in := &struct {
A websvc.JSONTime
}{
A: testJSONTime,
}
got, err := json.Marshal(in)
require.NoError(t, err)
assert.Equal(t, []byte(`{"A":`+testJSONTimeStr+`}`), got)
})
}
func TestJSONTime_UnmarshalJSON(t *testing.T) {
testCases := []struct {
name string
wantErrMsg string
want websvc.JSONTime
data []byte
}{{
name: "time",
wantErrMsg: "",
want: testJSONTime,
data: []byte(testJSONTimeStr),
}, {
name: "bad",
wantErrMsg: `parsing json time: strconv.ParseFloat: parsing "{}": ` +
`invalid syntax`,
want: websvc.JSONTime{},
data: []byte(`{}`),
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var got websvc.JSONTime
err := got.UnmarshalJSON(tc.data)
testutil.AssertErrorMsg(t, tc.wantErrMsg, err)
assert.Equal(t, tc.want, got)
})
}
t.Run("nil", func(t *testing.T) {
err := (*websvc.JSONTime)(nil).UnmarshalJSON([]byte("0"))
require.Error(t, err)
msg := err.Error()
assert.Equal(t, "json time is nil", msg)
})
t.Run("json", func(t *testing.T) {
want := testJSONTime
var got struct {
A websvc.JSONTime
}
err := json.Unmarshal([]byte(`{"A":`+testJSONTimeStr+`}`), &got)
require.NoError(t, err)
assert.Equal(t, want, got.A)
})
}

View File

@ -0,0 +1,11 @@
package websvc
// Path constants
const (
PathHealthCheck = "/health-check"
PathV1SettingsAll = "/api/v1/settings/all"
PathV1SettingsDNS = "/api/v1/settings/dns"
PathV1SettingsHTTP = "/api/v1/settings/http"
PathV1SystemInfo = "/api/v1/system/info"
)

View File

@ -0,0 +1,42 @@
package websvc
import (
"net/http"
)
// All Settings Handlers
// RespGetV1SettingsAll describes the response of the GET /api/v1/settings/all
// HTTP API.
type RespGetV1SettingsAll struct {
// TODO(a.garipov): Add more as we go.
DNS *HTTPAPIDNSSettings `json:"dns"`
HTTP *HTTPAPIHTTPSettings `json:"http"`
}
// handleGetSettingsAll is the handler for the GET /api/v1/settings/all HTTP
// API.
func (svc *Service) handleGetSettingsAll(w http.ResponseWriter, r *http.Request) {
dnsSvc := svc.confMgr.DNS()
dnsConf := dnsSvc.Config()
webSvc := svc.confMgr.Web()
httpConf := webSvc.Config()
// TODO(a.garipov): Add all currently supported parameters.
writeJSONOKResponse(w, r, &RespGetV1SettingsAll{
DNS: &HTTPAPIDNSSettings{
Addresses: dnsConf.Addresses,
BootstrapServers: dnsConf.BootstrapServers,
UpstreamServers: dnsConf.UpstreamServers,
UpstreamTimeout: JSONDuration(dnsConf.UpstreamTimeout),
},
HTTP: &HTTPAPIHTTPSettings{
Addresses: httpConf.Addresses,
SecureAddresses: httpConf.SecureAddresses,
Timeout: JSONDuration(httpConf.Timeout),
ForceHTTPS: httpConf.ForceHTTPS,
},
})
}

View File

@ -0,0 +1,75 @@
package websvc_test
import (
"crypto/tls"
"encoding/json"
"net/http"
"net/netip"
"net/url"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/next/agh"
"github.com/AdguardTeam/AdGuardHome/internal/next/dnssvc"
"github.com/AdguardTeam/AdGuardHome/internal/next/websvc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestService_HandleGetSettingsAll(t *testing.T) {
// TODO(a.garipov): Add all currently supported parameters.
wantDNS := &websvc.HTTPAPIDNSSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:53")},
BootstrapServers: []string{"94.140.14.140", "94.140.14.141"},
UpstreamServers: []string{"94.140.14.14", "1.1.1.1"},
UpstreamTimeout: websvc.JSONDuration(1 * time.Second),
}
wantWeb := &websvc.HTTPAPIHTTPSettings{
Addresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:80")},
SecureAddresses: []netip.AddrPort{netip.MustParseAddrPort("127.0.0.1:443")},
Timeout: websvc.JSONDuration(5 * time.Second),
ForceHTTPS: true,
}
confMgr := newConfigManager()
confMgr.onDNS = func() (s agh.ServiceWithConfig[*dnssvc.Config]) {
c, err := dnssvc.New(&dnssvc.Config{
Addresses: wantDNS.Addresses,
UpstreamServers: wantDNS.UpstreamServers,
BootstrapServers: wantDNS.BootstrapServers,
UpstreamTimeout: time.Duration(wantDNS.UpstreamTimeout),
})
require.NoError(t, err)
return c
}
confMgr.onWeb = func() (s agh.ServiceWithConfig[*websvc.Config]) {
return websvc.New(&websvc.Config{
TLS: &tls.Config{
Certificates: []tls.Certificate{{}},
},
Addresses: wantWeb.Addresses,
SecureAddresses: wantWeb.SecureAddresses,
Timeout: time.Duration(wantWeb.Timeout),
ForceHTTPS: true,
})
}
_, addr := newTestServer(t, confMgr)
u := &url.URL{
Scheme: "http",
Host: addr.String(),
Path: websvc.PathV1SettingsAll,
}
body := httpGet(t, u, http.StatusOK)
resp := &websvc.RespGetV1SettingsAll{}
err := json.Unmarshal(body, resp)
require.NoError(t, err)
assert.Equal(t, wantDNS, resp.DNS)
assert.Equal(t, wantWeb, resp.HTTP)
}

View File

@ -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(),
}) })
} }

View File

@ -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,
} }

View File

@ -0,0 +1,31 @@
package websvc
import (
"net"
"sync"
)
// Wait Listener
// waitListener is a wrapper around a listener that also calls wg.Done() on the
// first call to Accept. It is useful in situations where it is important to
// catch the precise moment of the first call to Accept, for example when
// starting an HTTP server.
//
// TODO(a.garipov): Move to aghnet?
type waitListener struct {
net.Listener
firstAcceptWG *sync.WaitGroup
firstAcceptOnce sync.Once
}
// type check
var _ net.Listener = (*waitListener)(nil)
// Accept implements the [net.Listener] interface for *waitListener.
func (l *waitListener) Accept() (conn net.Conn, err error) {
l.firstAcceptOnce.Do(l.firstAcceptWG.Done)
return l.Listener.Accept()
}

View File

@ -0,0 +1,46 @@
package websvc
import (
"net"
"sync"
"sync/atomic"
"testing"
"github.com/AdguardTeam/AdGuardHome/internal/aghchan"
"github.com/AdguardTeam/AdGuardHome/internal/aghtest"
"github.com/stretchr/testify/assert"
)
func TestWaitListener_Accept(t *testing.T) {
// TODO(a.garipov): use atomic.Bool in Go 1.19.
var numAcceptCalls uint32
var l net.Listener = &aghtest.Listener{
OnAccept: func() (conn net.Conn, err error) {
atomic.AddUint32(&numAcceptCalls, 1)
return nil, nil
},
OnAddr: func() (addr net.Addr) { panic("not implemented") },
OnClose: func() (err error) { panic("not implemented") },
}
wg := &sync.WaitGroup{}
wg.Add(1)
done := make(chan struct{})
go aghchan.MustReceive(done, testTimeout)
go func() {
var wrapper net.Listener = &waitListener{
Listener: l,
firstAcceptWG: wg,
}
_, _ = wrapper.Accept()
}()
wg.Wait()
close(done)
assert.Equal(t, uint32(1), atomic.LoadUint32(&numAcceptCalls))
}

View File

@ -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 {
confMgr ConfigManager
tls *tls.Config tls *tls.Config
servers []*http.Server
start time.Time start time.Time
servers []*http.Server
timeout time.Duration 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{
confMgr: c.ConfigManager,
tls: c.TLS, tls: c.TLS,
start: c.Start, start: c.Start,
timeout: c.Timeout, 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))
} }
return addrs // srv.Serve will set TLSConfig to an almost empty value, so, instead of
// relying only on the nilness of TLSConfig, check the length of the
// certificates field as well.
if srv.TLSConfig == nil || len(srv.TLSConfig.Certificates) == 0 {
addrs = append(addrs, addrPort)
} else {
secureAddrs = append(secureAddrs, addrPort)
}
}
return addrs, secureAddrs
} }
// handleGetHealthCheck is the handler for the GET /health-check HTTP API. // 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
}

View File

@ -0,0 +1,6 @@
package websvc
import "time"
// testTimeout is the common timeout for tests.
const testTimeout = 1 * time.Second

View File

@ -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)
}

View File

@ -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.

View File

@ -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

View File

@ -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 }

View File

@ -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