+
-
-
-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.
+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
+software for that.
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
-software we use for our public [AdGuard DNS](https://adguard-dns.io/) servers,
-and both share a lot of code.
+software we use for our public [AdGuard DNS] servers, and both share a lot of
+code.
+
+[AdGuard DNS]: https://adguard-dns.io/
-* [Getting Started](#getting-started)
-* [Comparing AdGuard Home to other solutions](#comparison)
- * [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 traditional ad blockers](#comparison-adblock)
-* [How to build from source](#how-to-build)
-* [Contributing](#contributing)
- * [Test unstable versions](#test-unstable-versions)
- * [Reporting issues](#reporting-issues)
- * [Help with translations](#translate)
- * [Other](#help-other)
-* [Projects that use AdGuard Home](#uses)
-* [Acknowledgments](#acknowledgments)
-* [Privacy](#privacy)
+ * [Getting Started](#getting-started)
+ * [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 does AdGuard Home compare to Pi-Hole](#comparison-pi-hole)
+ * [How does AdGuard Home compare to traditional ad blockers](#comparison-adblock)
+ * [Known limitations](#comparison-limitations)
+ * [How to build from source](#how-to-build)
+ * [Prerequisites](#prerequisites)
+ * [Building](#building)
+ * [Contributing](#contributing)
+ * [Test unstable versions](#test-unstable-versions)
+ * [Reporting issues](#reporting-issues)
+ * [Help with translations](#translate)
+ * [Other](#help-other)
+ * [Projects that use AdGuard Home](#uses)
+ * [Acknowledgments](#acknowledgments)
+ * [Privacy](#privacy)
-
-## Getting Started
-### Automated install (Linux and Mac)
+
+## Getting Started
+
+ ### Automated install (Unix)
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:
-* `-c ` to use specified channel.
-* `-r` to reinstall AdGuard Home;
-* `-u` to uninstall AdGuard Home;
-* `-v` for verbose output;
+
+ * `-c ` to use specified channel;
+ * `-r` to reinstall AdGuard Home;
+ * `-u` to uninstall AdGuard Home;
+ * `-v` for verbose output.
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.
+ ### Alternative methods
-#### Docker
+ #### Manual installation
-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
+ #### Docker
-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
+ #### Snap Store
-* [Getting Started](https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started)
- * [FAQ](https://github.com/AdguardTeam/AdGuardHome/wiki/FAQ)
- * [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)
+If you're running **Linux,** there's a secure and easy way to install AdGuard
+Home: get it from the [Snap Store].
-### 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/).
-
-## Comparing AdGuard Home to other solutions
-
-### How is this different from public AdGuard DNS servers?
+ ### Guides
-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.
-* 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.**
+[wiki]: https://github.com/AdguardTeam/AdGuardHome/wiki
-
-### 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.
+ ### API
-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/
+
+
+
+## Comparing AdGuard Home to other solutions
+
+ ### 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:
+
+ * 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.**
+
+
+
+ ### 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 the so-called “DNS sinkholing” method and both allow
+customizing what's blocked.
+
+
+
+AdGuard Home provides a lot of features out-of-the-box with no need to install
+and configure additional software. We want it to be simple to the point when
+even casual users can set it up with minimal effort.
+
+**Disclaimer:** some of the listed features can be added to Pi-Hole by
+installing additional software or by manually using SSH terminal and
+reconfiguring one of the utilities Pi-Hole consists of. However, in our
+opinion, this cannot be legitimately counted as a Pi-Hole's feature.
| Feature | AdGuard Home | Pi-Hole |
|-------------------------------------------------------------------------|-------------------|-----------------------------------------------------------|
@@ -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 | ✅ | ❌ |
| Per-client (device) configuration | ✅ | ✅ |
| 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] | ✅ | ❌ |
-
-### How does AdGuard Home compare to traditional ad blockers
+[wiki-noroot]: https://github.com/AdguardTeam/AdGuardHome/wiki/Getting-Started#running-without-superuser
+
+
+
+ ### How does AdGuard Home compare to traditional ad blockers
It depends.
-“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
-about the difference between these methods by reading
-[this article](https://adguard.com/en/blog/adguard-vs-adaway-dns66/). It
-compares AdGuard for Android (a traditional ad blocker) to hosts-level ad
-blockers (which are almost identical to DNS-based blockers in their
-capabilities). This level of protection is enough for some users.
+DNS sinkholing is capable of blocking a big percentage of ads, but it lacks
+the flexibility and the power of traditional ad blockers. You can get a good
+impression about the difference between these methods by reading [this
+article][blog-adaway], which compares AdGuard for Android (a traditional ad
+blocker) to hosts-level ad blockers (which are almost identical to DNS-based
+blockers in their capabilities). This level of protection is enough for some
+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).
-
-**Known limitations**
+ ### Known limitations
Here are some examples of what cannot be blocked by a DNS-level blocker:
-* YouTube, Twitch ads
-* Facebook, Twitter, Instagram sponsored posts
+ * YouTube, Twitch ads;
-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.
-
-## How to build from source
+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][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
+
+
+
+## How to build from source
+
+ ### Prerequisites
Run `make init` to prepare the development environment.
You will need this to build AdGuard Home:
- * [go](https://golang.org/dl/) v1.18 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).
- * [yarn](https://yarnpkg.com/) v1.22.5 or later.
+ * [Go](https://golang.org/dl/) v1.18 or later;
+ * [Node.js](https://nodejs.org/en/download/) v10.16.2 or later;
+ * [npm](https://www.npmjs.com/) v6.14 or later;
+ * [yarn](https://yarnpkg.com/) v1.22.5 or later.
-### Building
-Open Terminal and execute these commands:
+
+ ### Building
+
+Open your terminal and execute these commands:
```sh
git clone https://github.com/AdguardTeam/AdGuardHome
@@ -216,16 +266,18 @@ cd AdGuardHome
make
```
-Please note, that the non-standard `-j` flag is currently not supported, so
-building with `make -j 4` or setting your `MAKEFLAGS` to include, for example,
-`-j 4` is likely to break the build. If you do have your `MAKEFLAGS` set to
-that, and you don't want to change it, you can override it by running
-`make -j 1`.
+**NOTE:** The non-standard `-j` flag is currently not supported, so building
+with `make -j 4` or setting your `MAKEFLAGS` to include, for example, `-j 4` is
+likely to break the build. If you do have your `MAKEFLAGS` set to that, and you
+don't want to change it, you can override it by running `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.
-In order to do this, specify `GOOS` and `GOARCH` env variables before running make.
+ #### Building for a different platform
+
+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:
@@ -239,168 +291,223 @@ or:
make GOOS='linux' GOARCH='arm64'
```
-#### Preparing release
+ #### Preparing releases
-You'll need this to prepare a release build:
-
-* [snapcraft](https://snapcraft.io/)
-
-Commands:
+You'll need [`snapcraft`] to prepare a release build. Once installed, run the
+following command:
```sh
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).
+ #### Docker image
-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:
-* (Linux-only) Install Qemu: `docker run --rm --privileged multiarch/qemu-user-static --reset -p yes --credential yes`
-* Prepare builder: `docker buildx create --name buildx-builder --driver docker-container --use`
+ * (Linux-only) Install Qemu:
+
+ ```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].
+
+ #### Debugging the frontend
+
+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`
-* `scripts/whotracksme`
+## Contributing
-
-## Contributing
+You are welcome to fork this repository, make your changes and [submit a pull
+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
-
-### Test unstable versions
+
+
+ ### Test unstable versions
There are two update channels that you can use:
-* `beta` - beta version 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.
+ * `beta`: beta versions of AdGuard Home. More or less stable versions,
+ 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:
-1. [Snap Store](https://snapcraft.io/adguard-home) -- look for "beta" and "edge" channels there.
-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.
+1. [Snap Store]: look for the `beta` and `edge` channels.
-Beta:
+2. [Docker Hub]: look for the `beta` and `edge` tags.
-```sh
-curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
-```
+3. Standalone builds. Use the automated installation script or look for the
+ available builds [on the Wiki][wiki-platf].
-Edge:
+ Script to install a beta version:
-```sh
-curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
-```
+ ```sh
+ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c beta
+ ```
- * Beta channel builds
- * 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)
+ Script to install an edge version:
- * Edge channel builds
- * 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)
- * 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)
- * 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)
+ ```sh
+ curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -c edge
+ ```
+[wiki-platf]: https://github.com/AdguardTeam/AdGuardHome/wiki/Platforms
-
-### 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.
+ ### Report issues
-
-### Help with translations
+If you run into any problem or have a suggestion, head to [this page][iss] and
+click on the “New issue” button.
+
+[iss]: https://github.com/AdguardTeam/AdGuardHome/issues
+
+
+
+ ### Help with translations
If you want to help with AdGuard Home translations, please learn more about
-translating AdGuard products
-[in our Knowledge Base](https://kb.adguard.com/en/general/adguard-translations).
+translating AdGuard products [in our Knowledge Base][kb-trans]. You can
+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
+[kb-trans]: https://kb.adguard.com/en/general/adguard-translations
-
-### Other
-Here's what you can also do to contribute:
-1. [Look for issues][helpissues] marked as "help wanted".
-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].
+ ### Other
-[helpissues]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22+
-[blocked.go]: https://github.com/AdguardTeam/AdGuardHome/blob/master/internal/filtering/blocked.go
-[companiesdb]: https://github.com/AdguardTeam/companiesdb
-[filters.json]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/src/helpers/filters/filters.json
+Another way you can contribute is by [looking for issues][iss-help] marked as
+`help wanted`, asking if the issue is up for grabs, and sending a PR fixing the
+bug or implementing the feature.
-
-## Projects that use AdGuard Home
+[iss-help]: https://github.com/AdguardTeam/AdGuardHome/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22
-* [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/)
-
-## Acknowledgments
+
+## 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/).
+
+
+
+## Acknowledgments
+
+
This software wouldn't have been possible without:
- * [Go](https://golang.org/dl/) and its libraries:
- * [gcache](https://github.com/bluele/gcache)
- * [miekg's dns](https://github.com/miekg/dns)
- * [go-yaml](https://github.com/go-yaml/yaml)
- * [service](https://godoc.org/github.com/kardianos/service)
- * [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
- * [urlfilter](https://github.com/AdguardTeam/urlfilter)
- * [Node.js](https://nodejs.org/) and its libraries:
- * [React.js](https://reactjs.org)
- * [Tabler](https://github.com/tabler/tabler)
- * And many more node.js packages.
- * [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
+ * [Go](https://golang.org/dl/) and its libraries:
+ * [gcache](https://github.com/bluele/gcache)
+ * [miekg's dns](https://github.com/miekg/dns)
+ * [go-yaml](https://github.com/go-yaml/yaml)
+ * [service](https://godoc.org/github.com/kardianos/service)
+ * [dnsproxy](https://github.com/AdguardTeam/dnsproxy)
+ * [urlfilter](https://github.com/AdguardTeam/urlfilter)
+ * [Node.js](https://nodejs.org/) and its libraries:
+ * And many more Node.js packages.
+ * [React.js](https://reactjs.org)
+ * [Tabler](https://github.com/tabler/tabler)
+ * [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
-You might have seen that [CoreDNS](https://coredns.io) was mentioned here
-before, but we've stopped using it in AdGuard Home.
+You might have seen that [CoreDNS] was mentioned here before, but we've stopped
+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.
-
-## Privacy
+[CoreDNS]: https://coredns.io
+[src-packagejson]: https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json
+
+## Privacy
+
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,
-and does not use any web services unless you configure it to do so. Full policy
-with every bit that *could in theory be* sent by AdGuard Home is available
-[here](https://adguard.com/en/privacy/home.html)
+and does not use any web services unless you configure it to do so. See also
+the [full privacy policy][privacy] with every bit that *could in theory be sent*
+by AdGuard Home is available.
+
+[privacy]: https://adguard.com/en/privacy/home.html
From 4582b1c9198dcbc1927b74080839e81ba347265a Mon Sep 17 00:00:00 2001
From: Eugene Burkov
Date: Fri, 14 Oct 2022 15:29:44 +0300
Subject: [PATCH 18/20] Pull request: Migrate to netip.Addr vol.1
Merge in DNS/adguard-home from 2926-lla-v6 to master
Updates #2926.
Updates #5035.
Squashed commit of the following:
commit 2e770d4b6d4e1ec3f7762f2f2466662983bf146c
Merge: 25c1afc5 893358ea
Author: Eugene Burkov
Date: Fri Oct 14 15:14:56 2022 +0300
Merge branch 'master' into 2926-lla-v6
commit 25c1afc5f0a5027fafac9dee77618886aefee29c
Author: Eugene Burkov
Date: Thu Oct 13 18:24:20 2022 +0300
all: imp code, docs
commit 59549c4f74ee17b10eae542d1f1828d4e59894c9
Author: Eugene Burkov
Date: Tue Oct 11 18:49:09 2022 +0300
dhcpd: use netip initially
commit 1af623096b0517d07752385540f2f750f7f5b3bb
Author: Eugene Burkov
Date: Fri Sep 30 18:03:52 2022 +0300
all: imp docs, code
commit e9faeb71dbc0e887b25a7f3d5b33a401805f2ae7
Author: Eugene Burkov
Date: Thu Sep 29 14:56:37 2022 +0300
all: use netip for web
commit 38305e555a6884c3bd1b0839330b942ce0e59093
Author: Eugene Burkov
Date: Wed Sep 28 19:13:58 2022 +0300
add basic lla
---
CHANGELOG.md | 2 +
internal/aghnet/dhcp_unix.go | 31 +++---
internal/aghnet/hostscontainer.go | 4 +
internal/aghnet/net.go | 165 +++++++++++++++++-----------
internal/aghnet/net_linux.go | 14 +--
internal/aghnet/net_test.go | 137 +++++++++++------------
internal/dhcpd/config.go | 82 +++++++-------
internal/dhcpd/conn_unix.go | 4 +-
internal/dhcpd/dhcpd.go | 2 +-
internal/dhcpd/dhcpd_unix_test.go | 35 +++---
internal/dhcpd/http_unix.go | 42 ++++---
internal/dhcpd/iprange.go | 2 +
internal/dhcpd/options_unix.go | 7 +-
internal/dhcpd/options_unix_test.go | 2 -
internal/dhcpd/v4_unix.go | 25 +++--
internal/dhcpd/v4_unix_test.go | 81 ++++++++------
internal/dnsforward/access.go | 1 +
internal/dnsforward/dnsforward.go | 1 +
internal/home/clients.go | 2 +
internal/home/clients_test.go | 9 +-
internal/home/config.go | 57 ++++++----
internal/home/control.go | 12 +-
internal/home/controlinstall.go | 42 +++----
internal/home/dns.go | 27 ++---
internal/home/home.go | 5 +-
internal/home/mobileconfig_test.go | 5 +-
internal/home/options.go | 13 ++-
internal/home/options_test.go | 12 +-
internal/home/web.go | 11 +-
29 files changed, 465 insertions(+), 367 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31854726..97b119be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@ and this project adheres to
## Added
+- 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.
@@ -33,6 +34,7 @@ and this project adheres to
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
[#4942]: https://github.com/AdguardTeam/AdGuardHome/issues/4942
[#4986]: https://github.com/AdguardTeam/AdGuardHome/issues/4986
diff --git a/internal/aghnet/dhcp_unix.go b/internal/aghnet/dhcp_unix.go
index 464e5aa9..16c3c87a 100644
--- a/internal/aghnet/dhcp_unix.go
+++ b/internal/aghnet/dhcp_unix.go
@@ -6,6 +6,7 @@ import (
"bytes"
"fmt"
"net"
+ "net/netip"
"os"
"time"
@@ -38,48 +39,44 @@ func checkOtherDHCP(ifaceName string) (ok4, ok6 bool, err4, err6 error) {
}
// 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
if addrs, err = iface.Addrs(); err != nil {
- return nil, err
+ return netip.Prefix{}, err
}
for _, a := range addrs {
+ var ip net.IP
+ var maskLen int
switch a := a.(type) {
case *net.IPAddr:
- subnet = &net.IPNet{
- IP: a.IP,
- Mask: a.IP.DefaultMask(),
- }
+ ip = a.IP
+ maskLen, _ = ip.DefaultMask().Size()
case *net.IPNet:
- subnet = a
+ ip = a.IP
+ maskLen, _ = a.Mask.Size()
default:
continue
}
- if ip4 := subnet.IP.To4(); ip4 != nil {
- subnet.IP = ip4
-
- return subnet, nil
+ if ip = ip.To4(); ip != nil {
+ return netip.PrefixFrom(netip.AddrFrom4(*(*[4]byte)(ip)), maskLen), 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
// waits for a response for a period defined by defaultDiscoverTime.
func checkOtherDHCPv4(iface *net.Interface) (ok bool, err error) {
- var subnet *net.IPNet
+ var subnet netip.Prefix
if subnet, err = ifaceIPv4Subnet(iface); err != nil {
return false, err
}
// Resolve broadcast addr.
- dst := netutil.IPPort{
- IP: BroadcastFromIPNet(subnet),
- Port: 67,
- }.String()
+ dst := netip.AddrPortFrom(BroadcastFromPref(subnet), 67).String()
var dstAddr *net.UDPAddr
if dstAddr, err = net.ResolveUDPAddr("udp4", dst); err != nil {
return false, fmt.Errorf("couldn't resolve UDP address %s: %w", dst, err)
diff --git a/internal/aghnet/hostscontainer.go b/internal/aghnet/hostscontainer.go
index 5d750945..9b9124d9 100644
--- a/internal/aghnet/hostscontainer.go
+++ b/internal/aghnet/hostscontainer.go
@@ -106,9 +106,13 @@ type HostsContainer struct {
done chan struct{}
// updates is the channel for receiving updated hosts.
+ //
+ // TODO(e.burkov): Use map[netip.Addr]struct{} instead.
updates chan *netutil.IPMap
// 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
// fsys is the working file system to read hosts files from.
diff --git a/internal/aghnet/net.go b/internal/aghnet/net.go
index 2de9c630..bdcc6e4f 100644
--- a/internal/aghnet/net.go
+++ b/internal/aghnet/net.go
@@ -7,12 +7,12 @@ import (
"fmt"
"io"
"net"
+ "net/netip"
"syscall"
"github.com/AdguardTeam/AdGuardHome/internal/aghos"
"github.com/AdguardTeam/golibs/errors"
"github.com/AdguardTeam/golibs/log"
- "github.com/AdguardTeam/golibs/netutil"
)
// Variables and functions to substitute in tests.
@@ -31,6 +31,12 @@ var (
// the IP being static is available.
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.
// If it can't give a definitive answer, it returns false and an error for which
// 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
// 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)
if err != nil {
log.Debug("%s", err)
- return nil
+ return netip.Addr{}
} else if code != 0 {
log.Debug("fetching gateway ip: unexpected exit code: %d", code)
- return nil
+ return netip.Addr{}
}
fields := bytes.Fields(out)
// The meaningful "ip route" command output should contain the word
// "default" at first field and default gateway IP address at third field.
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
@@ -78,9 +89,9 @@ func CanBindPrivilegedPorts() (can bool, err error) {
// NetInterface represents an entry of network interfaces map.
type NetInterface struct {
// 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 []*net.IPNet `json:"-"`
+ Subnets []netip.Prefix `json:"-"`
Name string `json:"name"`
HardwareAddr net.HardwareAddr `json:"hardware_address"`
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
// WEB only we do not return link-local addresses here.
//
// 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.
-func GetValidNetInterfacesForWeb() (netIfaces []*NetInterface, err error) {
+func GetValidNetInterfacesForWeb() (nifaces []*NetInterface, err error) {
ifaces, err := net.Interfaces()
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 {
- return nil, errors.Error("couldn't find any legible interface")
+ return nil, errors.Error("no legible interfaces")
}
- for _, iface := range ifaces {
- var addrs []net.Addr
- addrs, err = iface.Addrs()
+ for i := range ifaces {
+ var niface *NetInterface
+ niface, err = NetInterfaceFrom(&ifaces[i])
if err != nil {
- return nil, fmt.Errorf("failed to get addresses for interface %s: %w", iface.Name, err)
- }
-
- 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.
- if len(netIface.Addresses) != 0 {
- netIfaces = append(netIfaces, netIface)
+ return nil, err
+ } else if len(niface.Addresses) != 0 {
+ // Discard interfaces with no addresses.
+ nifaces = append(nifaces, niface)
}
}
- return netIfaces, nil
+ return nifaces, nil
}
// 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.
//
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
-func InterfaceByIP(ip net.IP) (ifaceName string) {
+func InterfaceByIP(ip netip.Addr) (ifaceName string) {
ifaces, err := GetValidNetInterfacesForWeb()
if err != nil {
return ""
@@ -168,7 +201,7 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
for _, iface := range ifaces {
for _, addr := range iface.Addresses {
- if ip.Equal(addr) {
+ if ip == addr {
return iface.Name
}
}
@@ -177,15 +210,16 @@ func InterfaceByIP(ip net.IP) (ifaceName string) {
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.
//
// TODO(e.burkov): See TODO on GetValidNetInterfacesForWeb.
-func GetSubnet(ifaceName string) *net.IPNet {
+func GetSubnet(ifaceName string) (p netip.Prefix) {
netIfaces, err := GetValidNetInterfacesForWeb()
if err != nil {
log.Error("Could not get network interfaces info: %v", err)
- return nil
+
+ return p
}
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
// 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
- addr := netutil.IPPort{IP: ip, Port: port}.String()
+ addr := ipp.String()
switch network {
case "tcp":
c, err = net.Listen(network, addr)
@@ -251,18 +285,23 @@ func CollectAllIfacesAddrs() (addrs []string, err error) {
return addrs, nil
}
-// BroadcastFromIPNet calculates the broadcast IP address for n.
-func BroadcastFromIPNet(n *net.IPNet) (dc net.IP) {
- dc = netutil.CloneIP(n.IP)
-
- mask := n.Mask
- if mask == nil {
- mask = dc.DefaultMask()
+// BroadcastFromPref calculates the broadcast IP address for p.
+func BroadcastFromPref(p netip.Prefix) (bc netip.Addr) {
+ bc = p.Addr().Unmap()
+ if !bc.IsValid() {
+ return netip.Addr{}
}
- for i, b := range mask {
- dc[i] |= ^b
+ maskLen, addrLen := p.Bits(), bc.BitLen()
+ 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
}
diff --git a/internal/aghnet/net_linux.go b/internal/aghnet/net_linux.go
index d0c3f7fd..fc83d56a 100644
--- a/internal/aghnet/net_linux.go
+++ b/internal/aghnet/net_linux.go
@@ -6,7 +6,7 @@ import (
"bufio"
"fmt"
"io"
- "net"
+ "net/netip"
"os"
"strings"
@@ -151,7 +151,7 @@ func findIfaceLine(s *bufio.Scanner, name string) (ok bool) {
// interface through dhcpcd.conf.
func ifaceSetStaticIP(ifaceName string) (err error) {
ipNet := GetSubnet(ifaceName)
- if ipNet.IP == nil {
+ if !ipNet.Addr().IsValid() {
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
// 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{}
stringutil.WriteToBuilder(
b,
@@ -183,15 +183,15 @@ func dhcpcdConfIface(ifaceName string, ipNet *net.IPNet, gwIP net.IP) (conf stri
" added by AdGuard Home.\ninterface ",
ifaceName,
"\nstatic ip_address=",
- ipNet.String(),
+ subnet.String(),
"\n",
)
- if gwIP != nil {
- stringutil.WriteToBuilder(b, "static routers=", gwIP.String(), "\n")
+ if gateway != (netip.Addr{}) {
+ 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()
}
diff --git a/internal/aghnet/net_test.go b/internal/aghnet/net_test.go
index d4ee59ee..730f1b4e 100644
--- a/internal/aghnet/net_test.go
+++ b/internal/aghnet/net_test.go
@@ -6,6 +6,7 @@ import (
"fmt"
"io/fs"
"net"
+ "net/netip"
"os"
"strings"
"testing"
@@ -93,34 +94,29 @@ func TestGatewayIP(t *testing.T) {
const cmd = "ip route show dev " + ifaceName
testCases := []struct {
- name string
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),
- 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),
- want: net.IP{
- 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x0, 0x0, 0x0,
- 0x0, 0x0, 0xFF, 0xFF,
- },
+ want: netip.MustParseAddr("::ffff"),
+ name: "success_v6",
}, {
- name: "bad_output",
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")),
- want: nil,
+ want: netip.Addr{},
+ name: "err_runcmd",
}, {
- name: "bad_code",
shell: theOnlyCmd(cmd, 1, "", nil),
- want: nil,
+ want: netip.Addr{},
+ name: "bad_code",
}}
for _, tc := range testCases {
@@ -150,65 +146,64 @@ func TestInterfaceByIP(t *testing.T) {
}
func TestBroadcastFromIPNet(t *testing.T) {
- known6 := net.IP{
- 1, 2, 3, 4,
- 5, 6, 7, 8,
- 9, 10, 11, 12,
- 13, 14, 15, 16,
- }
+ known4 := netip.MustParseAddr("192.168.0.1")
+ fullBroadcast4 := netip.MustParseAddr("255.255.255.255")
+
+ known6 := netip.MustParseAddr("102:304:506:708:90a:b0c:d0e:f10")
testCases := []struct {
- name string
- subnet *net.IPNet
- want net.IP
+ pref netip.Prefix
+ want netip.Addr
+ name string
}{{
+ pref: netip.PrefixFrom(known4, 0),
+ want: fullBroadcast4,
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",
- subnet: &net.IPNet{
- IP: known6,
- },
+ pref: netip.PrefixFrom(known4, 20),
+ want: netip.MustParseAddr("192.168.15.255"),
+ name: "full",
+ }, {
+ pref: netip.PrefixFrom(known6, netutil.IPv6BitLen),
want: known6,
+ name: "ipv6_no_mask",
}, {
+ pref: netip.PrefixFrom(known4, netutil.IPv4BitLen),
+ want: known4,
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",
- subnet: &net.IPNet{
- IP: net.IP{0, 0, 0, 0},
- Mask: net.IPMask{0, 0, 0, 0},
- },
- want: net.IPv4bcast,
+ }, {
+ pref: netip.Prefix{},
+ want: netip.Addr{},
+ name: "invalid",
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- bc := BroadcastFromIPNet(tc.subnet)
- assert.True(t, bc.Equal(tc.want), bc)
+ assert.Equal(t, tc.want, BroadcastFromPref(tc.pref))
})
}
}
func TestCheckPort(t *testing.T) {
+ laddr := netip.AddrPortFrom(IPv4Localhost(), 0)
+
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)
testutil.CleanupAndRequireSuccess(t, l.Close)
- ipp := netutil.IPPortFromAddr(l.Addr())
- require.NotNil(t, ipp)
- require.NotNil(t, ipp.IP)
- require.NotZero(t, ipp.Port)
+ addr := l.Addr()
+ require.IsType(t, new(net.TCPAddr), addr)
- 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{}
require.ErrorAs(t, err, &target)
@@ -216,16 +211,18 @@ func TestCheckPort(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)
testutil.CleanupAndRequireSuccess(t, conn.Close)
- ipp := netutil.IPPortFromAddr(conn.LocalAddr())
- require.NotNil(t, ipp)
- require.NotNil(t, ipp.IP)
- require.NotZero(t, ipp.Port)
+ addr := conn.LocalAddr()
+ require.IsType(t, new(net.UDPAddr), addr)
- 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{}
require.ErrorAs(t, err, &target)
@@ -233,12 +230,12 @@ func TestCheckPort(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)
})
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)
})
}
@@ -322,18 +319,18 @@ func TestNetInterface_MarshalJSON(t *testing.T) {
`"mtu":1500` +
`}` + "\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}
- mask4, mask6 := net.CIDRMask(24, netutil.IPv4BitLen), net.CIDRMask(8, netutil.IPv6BitLen)
+ ip4, ok := netip.AddrFromSlice([]byte{1, 2, 3, 4})
+ 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{
- Addresses: []net.IP{ip4, ip6},
- Subnets: []*net.IPNet{{
- IP: ip4.Mask(mask4),
- Mask: mask4,
- }, {
- IP: ip6.Mask(mask6),
- Mask: mask6,
- }},
+ Addresses: []netip.Addr{ip4, ip6},
+ Subnets: []netip.Prefix{net4, net6},
Name: "iface0",
HardwareAddr: net.HardwareAddr{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF},
Flags: net.FlagUp | net.FlagMulticast,
diff --git a/internal/dhcpd/config.go b/internal/dhcpd/config.go
index 9d8ef057..718d567f 100644
--- a/internal/dhcpd/config.go
+++ b/internal/dhcpd/config.go
@@ -3,12 +3,12 @@ package dhcpd
import (
"fmt"
"net"
+ "net/netip"
"time"
"github.com/AdguardTeam/AdGuardHome/internal/aghhttp"
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
"github.com/AdguardTeam/golibs/errors"
- "github.com/AdguardTeam/golibs/netutil"
)
// ServerConfig is the configuration for the DHCP server. The order of YAML
@@ -65,16 +65,16 @@ type V4ServerConf struct {
Enabled bool `yaml:"-" json:"-"`
InterfaceName string `yaml:"-" json:"-"`
- GatewayIP net.IP `yaml:"gateway_ip" json:"gateway_ip"`
- SubnetMask net.IP `yaml:"subnet_mask" json:"subnet_mask"`
+ GatewayIP netip.Addr `yaml:"gateway_ip" json:"gateway_ip"`
+ SubnetMask netip.Addr `yaml:"subnet_mask" json:"subnet_mask"`
// broadcastIP is the broadcasting address pre-calculated from the
// configured gateway IP and subnet mask.
- broadcastIP net.IP
+ broadcastIP netip.Addr
// The first & the last IP address for dynamic leases
// Bytes [0..2] of the last allowed IP address must match the first IP
- RangeStart net.IP `yaml:"range_start" json:"range_start"`
- RangeEnd net.IP `yaml:"range_end" json:"range_end"`
+ RangeStart netip.Addr `yaml:"range_start" json:"range_start"`
+ RangeEnd netip.Addr `yaml:"range_end" json:"range_end"`
LeaseDuration uint32 `yaml:"lease_duration" json:"lease_duration"` // in seconds
@@ -95,11 +95,11 @@ type V4ServerConf struct {
ipRange *ipRange
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
// gateway.
- subnet *net.IPNet
+ subnet netip.Prefix
// notify is a way to signal to other components that leases have been
// 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.
const errNilConfig errors.Error = "nil config"
-// ensureV4 returns a 4-byte version of ip. An error is returned if the passed
-// ip is not an IPv4.
-func ensureV4(ip net.IP) (ip4 net.IP, err error) {
- if ip == nil {
- return nil, fmt.Errorf("%v is not an IP address", ip)
- }
-
- ip4 = ip.To4()
- if ip4 == nil {
- return nil, fmt.Errorf("%v is not an IPv4 address", ip)
+// ensureV4 returns an unmapped version of ip. An error is returned if the
+// passed ip is not an IPv4.
+func ensureV4(ip netip.Addr, kind string) (ip4 netip.Addr, err error) {
+ ip4 = ip.Unmap()
+ if !ip4.IsValid() || !ip4.Is4() {
+ return netip.Addr{}, fmt.Errorf("%v is not an IPv4 %s", ip, kind)
}
return ip4, nil
@@ -139,33 +135,45 @@ func (c *V4ServerConf) Validate() (err error) {
return errNilConfig
}
- var gatewayIP net.IP
- gatewayIP, err = ensureV4(c.GatewayIP)
+ gatewayIP, err := ensureV4(c.GatewayIP, "address")
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
}
- if c.SubnetMask == nil {
- 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)
+ subnetMask, err := ensureV4(c.SubnetMask, "subnet mask")
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.
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",
gatewayIP,
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",
c.RangeStart,
c.subnet,
)
}
- if !c.subnet.Contains(c.RangeEnd) {
+ if !c.subnet.Contains(rangeEnd) {
return fmt.Errorf("range end %v is outside network %v",
c.RangeEnd,
c.subnet,
diff --git a/internal/dhcpd/conn_unix.go b/internal/dhcpd/conn_unix.go
index ec58afda..efcbc8aa 100644
--- a/internal/dhcpd/conn_unix.go
+++ b/internal/dhcpd/conn_unix.go
@@ -73,10 +73,10 @@ func (s *v4Server) newDHCPConn(iface *net.Interface) (c net.PacketConn, err erro
return &dhcpConn{
udpConn: bcast,
- bcastIP: s.conf.broadcastIP,
+ bcastIP: s.conf.broadcastIP.AsSlice(),
rawConn: ucast,
srcMAC: iface.HardwareAddr,
- srcIP: s.conf.dnsIPAddrs[0],
+ srcIP: s.conf.dnsIPAddrs[0].AsSlice(),
}, nil
}
diff --git a/internal/dhcpd/dhcpd.go b/internal/dhcpd/dhcpd.go
index 875b3d79..a98178ca 100644
--- a/internal/dhcpd/dhcpd.go
+++ b/internal/dhcpd/dhcpd.go
@@ -242,7 +242,7 @@ func Create(conf *ServerConfig) (s *server, err error) {
v4conf := conf.Conf4
v4conf.InterfaceName = s.conf.InterfaceName
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)
if err != nil {
diff --git a/internal/dhcpd/dhcpd_unix_test.go b/internal/dhcpd/dhcpd_unix_test.go
index cd1ca39a..f13dbf98 100644
--- a/internal/dhcpd/dhcpd_unix_test.go
+++ b/internal/dhcpd/dhcpd_unix_test.go
@@ -4,6 +4,7 @@ package dhcpd
import (
"net"
+ "net/netip"
"os"
"testing"
"time"
@@ -33,10 +34,10 @@ func TestDB(t *testing.T) {
s.srv4, err = v4Create(&V4ServerConf{
Enabled: true,
- RangeStart: net.IP{192, 168, 10, 100},
- RangeEnd: net.IP{192, 168, 10, 200},
- GatewayIP: net.IP{192, 168, 10, 1},
- SubnetMask: net.IP{255, 255, 255, 0},
+ RangeStart: netip.MustParseAddr("192.168.10.100"),
+ RangeEnd: netip.MustParseAddr("192.168.10.200"),
+ GatewayIP: netip.MustParseAddr("192.168.10.1"),
+ SubnetMask: netip.MustParseAddr("255.255.255.0"),
notify: testNotify,
})
require.NoError(t, err)
@@ -113,35 +114,35 @@ func TestNormalizeLeases(t *testing.T) {
func TestV4Server_badRange(t *testing.T) {
testCases := []struct {
name string
+ gatewayIP netip.Addr
+ subnetMask netip.Addr
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: " +
"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 " +
"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 " +
"192.168.10.1/27",
- gatewayIP: net.IP{192, 168, 10, 1},
- subnetMask: net.IP{255, 255, 255, 224},
}}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
conf := V4ServerConf{
Enabled: true,
- RangeStart: net.IP{192, 168, 10, 20},
- RangeEnd: net.IP{192, 168, 10, 200},
+ RangeStart: netip.MustParseAddr("192.168.10.20"),
+ RangeEnd: netip.MustParseAddr("192.168.10.200"),
GatewayIP: tc.gatewayIP,
SubnetMask: tc.subnetMask,
notify: testNotify,
diff --git a/internal/dhcpd/http_unix.go b/internal/dhcpd/http_unix.go
index e6b1f8fc..2e7ef57d 100644
--- a/internal/dhcpd/http_unix.go
+++ b/internal/dhcpd/http_unix.go
@@ -7,6 +7,7 @@ import (
"fmt"
"net"
"net/http"
+ "net/netip"
"os"
"github.com/AdguardTeam/AdGuardHome/internal/aghalg"
@@ -17,11 +18,11 @@ import (
)
type v4ServerConfJSON struct {
- GatewayIP net.IP `json:"gateway_ip"`
- SubnetMask net.IP `json:"subnet_mask"`
- RangeStart net.IP `json:"range_start"`
- RangeEnd net.IP `json:"range_end"`
- LeaseDuration uint32 `json:"lease_duration"`
+ GatewayIP netip.Addr `json:"gateway_ip"`
+ SubnetMask netip.Addr `json:"subnet_mask"`
+ RangeStart netip.Addr `json:"range_start"`
+ RangeEnd netip.Addr `json:"range_end"`
+ LeaseDuration uint32 `json:"lease_duration"`
}
func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
@@ -39,8 +40,8 @@ func (j *v4ServerConfJSON) toServerConf() *V4ServerConf {
}
type v6ServerConfJSON struct {
- RangeStart net.IP `json:"range_start"`
- LeaseDuration uint32 `json:"lease_duration"`
+ RangeStart netip.Addr `json:"range_start"`
+ LeaseDuration uint32 `json:"lease_duration"`
}
func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
@@ -49,7 +50,7 @@ func v6JSONToServerConf(j *v6ServerConfJSON) V6ServerConf {
}
return V6ServerConf{
- RangeStart: j.RangeStart,
+ RangeStart: j.RangeStart.AsSlice(),
LeaseDuration: j.LeaseDuration,
}
}
@@ -144,7 +145,7 @@ func (s *server) handleDHCPSetConfigV4(
v4Conf := conf.V4.toServerConf()
v4Conf.Enabled = conf.Enabled == aghalg.NBTrue
- if len(v4Conf.RangeStart) == 0 {
+ if !v4Conf.RangeStart.IsValid() {
v4Conf.Enabled = false
}
@@ -275,12 +276,12 @@ func (s *server) setConfFromJSON(conf *dhcpServerConfigJSON, srv4, srv6 DHCPServ
}
type netInterfaceJSON struct {
- Name string `json:"name"`
- HardwareAddr string `json:"hardware_address"`
- Flags string `json:"flags"`
- GatewayIP net.IP `json:"gateway_ip"`
- Addrs4 []net.IP `json:"ipv4_addresses"`
- Addrs6 []net.IP `json:"ipv6_addresses"`
+ Name string `json:"name"`
+ HardwareAddr string `json:"hardware_address"`
+ Flags string `json:"flags"`
+ GatewayIP netip.Addr `json:"gateway_ip"`
+ Addrs4 []netip.Addr `json:"ipv4_addresses"`
+ Addrs6 []netip.Addr `json:"ipv6_addresses"`
}
func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
@@ -341,13 +342,18 @@ func (s *server) handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
return
}
// ignore link-local
+ //
+ // TODO(e.burkov): Try to listen DHCP on LLA as well.
if ipnet.IP.IsLinkLocalUnicast() {
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 {
- 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 {
diff --git a/internal/dhcpd/iprange.go b/internal/dhcpd/iprange.go
index 45422957..6cf7d01f 100644
--- a/internal/dhcpd/iprange.go
+++ b/internal/dhcpd/iprange.go
@@ -27,6 +27,8 @@ const maxRangeLen = math.MaxUint32
// newIPRange creates a new IP address range. start must be less than end. The
// resulting range must not be greater than maxRangeLen.
+//
+// TODO(e.burkov): Use netip.Addr.
func newIPRange(start, end net.IP) (r *ipRange, err error) {
defer func() { err = errors.Annotate(err, "invalid ip range: %w") }()
diff --git a/internal/dhcpd/options_unix.go b/internal/dhcpd/options_unix.go
index 6950604d..082826d5 100644
--- a/internal/dhcpd/options_unix.go
+++ b/internal/dhcpd/options_unix.go
@@ -372,12 +372,9 @@ func (s *v4Server) prepareOptions() {
dhcpv4.OptGeneric(dhcpv4.OptionTCPKeepaliveGarbage, []byte{0x01}),
// Values From Configuration
+ dhcpv4.OptRouter(s.conf.GatewayIP.AsSlice()),
- // Set the Router Option to working subnet's IP since it's initialized
- // with the address of the gateway.
- dhcpv4.OptRouter(s.conf.subnet.IP),
-
- dhcpv4.OptSubnetMask(s.conf.subnet.Mask),
+ dhcpv4.OptSubnetMask(s.conf.SubnetMask.AsSlice()),
)
// Set values for explicitly configured options.
diff --git a/internal/dhcpd/options_unix_test.go b/internal/dhcpd/options_unix_test.go
index 2b5a5cb0..a4ab7dc8 100644
--- a/internal/dhcpd/options_unix_test.go
+++ b/internal/dhcpd/options_unix_test.go
@@ -251,8 +251,6 @@ func TestPrepareOptions(t *testing.T) {
for _, tc := range testCases {
s := &v4Server{
conf: &V4ServerConf{
- // Just to avoid nil pointer dereference.
- subnet: &net.IPNet{},
Options: tc.opts,
},
}
diff --git a/internal/dhcpd/v4_unix.go b/internal/dhcpd/v4_unix.go
index 3735ffdc..af389fda 100644
--- a/internal/dhcpd/v4_unix.go
+++ b/internal/dhcpd/v4_unix.go
@@ -6,6 +6,7 @@ import (
"bytes"
"fmt"
"net"
+ "net/netip"
"strings"
"sync"
"time"
@@ -295,7 +296,8 @@ func (s *v4Server) addLease(l *Lease) (err error) {
if l.IsStatic() {
// TODO(a.garipov, d.seregin): Subnet can be nil when dhcp server is
// 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)
}
} else if !inOffset {
@@ -353,7 +355,7 @@ func (s *v4Server) AddStaticLease(l *Lease) (err error) {
ip := l.IP.To4()
if ip == nil {
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)
}
@@ -701,7 +703,8 @@ func (s *v4Server) handleSelecting(
// Client inserts the address of the selected server in server identifier,
// ciaddr MUST be zero.
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)
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) {
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)
return nil, false
@@ -747,7 +751,7 @@ func (s *v4Server) handleInitReboot(req *dhcpv4.DHCPv4, reqIP net.IP) (l *Lease,
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
// 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)
@@ -972,7 +976,7 @@ func (s *v4Server) handle(req, resp *dhcpv4.DHCPv4) int {
// Include server's identifier option since any reply should contain it.
//
// 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.
var l *Lease
@@ -1188,7 +1192,14 @@ func (s *v4Server) Start() (err error) {
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
if c, err = s.newDHCPConn(iface); err != nil {
diff --git a/internal/dhcpd/v4_unix_test.go b/internal/dhcpd/v4_unix_test.go
index c73009a2..c8e4dd1f 100644
--- a/internal/dhcpd/v4_unix_test.go
+++ b/internal/dhcpd/v4_unix_test.go
@@ -5,6 +5,7 @@ package dhcpd
import (
"fmt"
"net"
+ "net/netip"
"strings"
"testing"
"time"
@@ -22,11 +23,11 @@ import (
)
var (
- DefaultRangeStart = net.IP{192, 168, 10, 100}
- DefaultRangeEnd = net.IP{192, 168, 10, 200}
- DefaultGatewayIP = net.IP{192, 168, 10, 1}
- DefaultSelfIP = net.IP{192, 168, 10, 2}
- DefaultSubnetMask = net.IP{255, 255, 255, 0}
+ DefaultRangeStart = netip.MustParseAddr("192.168.10.100")
+ DefaultRangeEnd = netip.MustParseAddr("192.168.10.200")
+ DefaultGatewayIP = netip.MustParseAddr("192.168.10.1")
+ DefaultSelfIP = netip.MustParseAddr("192.168.10.2")
+ DefaultSubnetMask = netip.MustParseAddr("255.255.255.0")
)
// defaultV4ServerConf returns the default configuration for *v4Server to use in
@@ -39,7 +40,7 @@ func defaultV4ServerConf() (conf *V4ServerConf) {
GatewayIP: DefaultGatewayIP,
SubnetMask: DefaultSubnetMask,
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),
Hostname: staticName,
HWAddr: anotherMAC,
- IP: anotherIP,
+ IP: anotherIP.AsSlice(),
})
assert.ErrorIs(t, err, ErrDupHostname)
})
@@ -96,7 +97,7 @@ func TestV4Server_leasing(t *testing.T) {
Expiry: time.Unix(leaseExpireStatic, 0),
Hostname: anotherName,
HWAddr: staticMAC,
- IP: anotherIP,
+ IP: anotherIP.AsSlice(),
})
testutil.AssertErrorMsg(t, wantErrMsg, err)
})
@@ -135,7 +136,7 @@ func TestV4Server_leasing(t *testing.T) {
dhcpv4.WithOption(dhcpv4.OptHostName(name)),
dhcpv4.WithOption(dhcpv4.OptRequestedIPAddress(ip)),
dhcpv4.WithOption(dhcpv4.OptClientIdentifier([]byte{1, 2, 3, 4, 5, 6, 8})),
- dhcpv4.WithGatewayIP(DefaultGatewayIP),
+ dhcpv4.WithGatewayIP(DefaultGatewayIP.AsSlice()),
)
require.NoError(t, err)
@@ -150,7 +151,7 @@ func TestV4Server_leasing(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(
dhcpv4.OptHostName(staticName),
@@ -164,7 +165,7 @@ func TestV4Server_leasing(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(
dhcpv4.OptHostName(anotherName),
@@ -219,7 +220,7 @@ func TestV4Server_AddRemove_static(t *testing.T) {
lease: &Lease{
Hostname: "probably-router.local",
HWAddr: net.HardwareAddr{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA},
- IP: DefaultGatewayIP,
+ IP: DefaultGatewayIP.AsSlice(),
},
name: "with_gateway_ip",
wantErrMsg: "dhcpv4: adding static lease: " +
@@ -326,7 +327,7 @@ func TestV4_AddReplace(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}
// 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()}
} else {
- defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP)) }()
+ defer func() { s.implicitOpts.Update(dhcpv4.OptDNS(defaultIP.AsSlice())) }()
}
var err error
s, err = v4Create(conf)
require.NoError(t, err)
- s.conf.dnsIPAddrs = []net.IP{defaultIP}
+ s.conf.dnsIPAddrs = []netip.Addr{defaultIP}
return s
}
@@ -386,7 +387,7 @@ func TestV4Server_handle_optionsPriority(t *testing.T) {
t.Run("default", func(t *testing.T) {
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) {
@@ -506,8 +507,9 @@ func TestV4StaticLease_Get(t *testing.T) {
s, ok := sIface.(*v4Server)
require.True(t, ok)
- s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
- s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
+ dnsAddr := netip.MustParseAddr("192.168.10.1")
+ s.conf.dnsIPAddrs = []netip.Addr{dnsAddr}
+ s.implicitOpts.Update(dhcpv4.OptDNS(dnsAddr.AsSlice()))
l := &Lease{
Hostname: "static-1.local",
@@ -539,9 +541,12 @@ func TestV4StaticLease_Get(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeOffer, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr)
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.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
+
+ assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
+ 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())
})
@@ -561,16 +566,19 @@ func TestV4StaticLease_Get(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
assert.Equal(t, mac, resp.ClientHWAddr)
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.Equal(t, s.conf.subnet.Mask, resp.SubnetMask())
+
+ assert.True(t, resp.Router()[0].Equal(s.conf.GatewayIP.AsSlice()))
+ 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())
})
dnsAddrs := resp.DNS()
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) {
ls := s.GetLeases(LeasesStatic)
@@ -591,8 +599,8 @@ func TestV4DynamicLease_Get(t *testing.T) {
s, err := v4Create(conf)
require.NoError(t, err)
- s.conf.dnsIPAddrs = []net.IP{{192, 168, 10, 1}}
- s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs...))
+ s.conf.dnsIPAddrs = []netip.Addr{netip.MustParseAddr("192.168.10.1")}
+ s.implicitOpts.Update(dhcpv4.OptDNS(s.conf.dnsIPAddrs[0].AsSlice()))
var req, resp *dhcpv4.DHCPv4
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, mac, resp.ClientHWAddr)
- assert.Equal(t, s.conf.RangeStart, resp.YourIPAddr)
- assert.Equal(t, s.conf.GatewayIP, resp.ServerIdentifier())
+ assert.True(t, resp.YourIPAddr.Equal(s.conf.RangeStart.AsSlice()))
+ assert.True(t, resp.ServerIdentifier().Equal(s.conf.GatewayIP.AsSlice()))
router := resp.Router()
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, []byte("012"), resp.Options.Get(dhcpv4.OptionFQDN))
@@ -649,15 +658,17 @@ func TestV4DynamicLease_Get(t *testing.T) {
t.Run("ack", func(t *testing.T) {
assert.Equal(t, dhcpv4.MessageTypeAck, resp.MessageType())
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()
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.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())
})
diff --git a/internal/dnsforward/access.go b/internal/dnsforward/access.go
index 23ec1137..ddf3d93f 100644
--- a/internal/dnsforward/access.go
+++ b/internal/dnsforward/access.go
@@ -19,6 +19,7 @@ import (
// accessCtx controls IP and client blocking that takes place before all other
// processing. An accessCtx is safe for concurrent use.
type accessCtx struct {
+ // TODO(e.burkov): Use map[netip.Addr]struct{} instead.
allowedIPs *netutil.IPMap
blockedIPs *netutil.IPMap
diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go
index 3806ef5b..2455fff5 100644
--- a/internal/dnsforward/dnsforward.go
+++ b/internal/dnsforward/dnsforward.go
@@ -81,6 +81,7 @@ type Server struct {
tableHostToIP hostToIPTable
tableHostToIPLock sync.Mutex
+ // TODO(e.burkov): Use map[netip.Addr]struct{} instead.
tableIPToHost *netutil.IPMap
tableIPToHostLock sync.Mutex
diff --git a/internal/home/clients.go b/internal/home/clients.go
index 631d27ba..ee535ea9 100644
--- a/internal/home/clients.go
+++ b/internal/home/clients.go
@@ -119,6 +119,8 @@ type clientsContainer struct {
idIndex map[string]*Client // ID -> client
// ipToRC is the IP address to *RuntimeClient map.
+ //
+ // TODO(e.burkov): Use map[netip.Addr]struct{} instead.
ipToRC *netutil.IPMap
lock sync.Mutex
diff --git a/internal/home/clients_test.go b/internal/home/clients_test.go
index 5b4ccdd3..00148c26 100644
--- a/internal/home/clients_test.go
+++ b/internal/home/clients_test.go
@@ -2,6 +2,7 @@ package home
import (
"net"
+ "net/netip"
"os"
"runtime"
"testing"
@@ -287,10 +288,10 @@ func TestClientsAddExisting(t *testing.T) {
DBFilePath: "leases.db",
Conf4: dhcpd.V4ServerConf{
Enabled: true,
- GatewayIP: net.IP{1, 2, 3, 1},
- SubnetMask: net.IP{255, 255, 255, 0},
- RangeStart: net.IP{1, 2, 3, 2},
- RangeEnd: net.IP{1, 2, 3, 10},
+ GatewayIP: netip.MustParseAddr("1.2.3.1"),
+ SubnetMask: netip.MustParseAddr("255.255.255.0"),
+ RangeStart: netip.MustParseAddr("1.2.3.2"),
+ RangeEnd: netip.MustParseAddr("1.2.3.10"),
},
}
diff --git a/internal/home/config.go b/internal/home/config.go
index b5aecea2..7df4f853 100644
--- a/internal/home/config.go
+++ b/internal/home/config.go
@@ -3,7 +3,7 @@ package home
import (
"bytes"
"fmt"
- "net"
+ "net/netip"
"os"
"path/filepath"
"sync"
@@ -85,19 +85,28 @@ type configuration struct {
// It's reset after config is parsed
fileData []byte
- BindHost net.IP `yaml:"bind_host"` // BindHost is the IP address of the HTTP server to bind to
- BindPort int `yaml:"bind_port"` // BindPort is the port the HTTP server
- BetaBindPort int `yaml:"beta_bind_port"` // BetaBindPort is the port for new client
- Users []webUser `yaml:"users"` // Users that can access HTTP server
+ // BindHost is the address for the web interface server to listen on.
+ BindHost netip.Addr `yaml:"bind_host"`
+ // BindPort is the port for the web interface server to listen on.
+ 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
// can do before being blocked.
AuthAttempts uint `yaml:"auth_attempts"`
// AuthBlockMin is the duration, in minutes, of the block of new login
// attempts after AuthAttempts unsuccessful login attempts.
- AuthBlockMin uint `yaml:"block_auth_min"`
- ProxyURL string `yaml:"http_proxy"` // Proxy address for our HTTP client
- Language string `yaml:"language"` // two-letter ISO 639-1 language code
- DebugPProf bool `yaml:"debug_pprof"` // Enable pprof HTTP server on port 6060
+ AuthBlockMin uint `yaml:"block_auth_min"`
+ // ProxyURL is the address of proxy server for the internal HTTP client.
+ ProxyURL string `yaml:"http_proxy"`
+ // 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)
// 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
// only configuration subsection covering the changes with a single
- // migration.
+ // migration. Also keep the blocked services in mind.
Filters []filtering.FilterYAML `yaml:"filters"`
WhitelistFilters []filtering.FilterYAML `yaml:"whitelist_filters"`
UserRules []string `yaml:"user_rules"`
@@ -135,18 +144,26 @@ type configuration struct {
// field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct {
- BindHosts []net.IP `yaml:"bind_hosts"`
- Port int `yaml:"port"`
+ BindHosts []netip.Addr `yaml:"bind_hosts"`
+ 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"`
- QueryLogEnabled bool `yaml:"querylog_enabled"` // if true, query log is enabled
- QueryLogFileEnabled bool `yaml:"querylog_file_enabled"` // if true, query log will be written to a file
+ // QueryLogEnabled defines if the query log is enabled.
+ 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 timeutil.Duration `yaml:"querylog_interval"`
- QueryLogMemSize uint32 `yaml:"querylog_size_memory"` // number of entries kept in memory before they are flushed to disk
- AnonymizeClientIP bool `yaml:"anonymize_client_ip"` // anonymize clients' IP addresses in logs and stats
+ QueryLogInterval timeutil.Duration `yaml:"querylog_interval"`
+ // QueryLogMemSize is the number of entries kept in memory before they are
+ // 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"`
@@ -211,12 +228,12 @@ type tlsConfigSettings struct {
var config = &configuration{
BindPort: 3000,
BetaBindPort: 0,
- BindHost: net.IP{0, 0, 0, 0},
+ BindHost: netip.IPv4Unspecified(),
AuthAttempts: 5,
AuthBlockMin: 15,
WebSessionTTLHours: 30 * 24,
DNS: dnsConfig{
- BindHosts: []net.IP{{0, 0, 0, 0}},
+ BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS,
StatsInterval: 1,
QueryLogEnabled: true,
diff --git a/internal/home/control.go b/internal/home/control.go
index 48ac45f4..5e4e6df2 100644
--- a/internal/home/control.go
+++ b/internal/home/control.go
@@ -2,8 +2,8 @@ package home
import (
"fmt"
- "net"
"net/http"
+ "net/netip"
"net/url"
"runtime"
"strings"
@@ -20,11 +20,11 @@ import (
// appendDNSAddrs is a convenient helper for appending a formatted form of DNS
// 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 {
var hostport string
if config.DNS.Port != defaultPortDNS {
- hostport = netutil.JoinHostPort(addr.String(), config.DNS.Port)
+ hostport = netip.AddrPortFrom(addr, uint16(config.DNS.Port)).String()
} else {
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
// dst. It also adds the IP addresses of all network interfaces if src contains
// 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
for _, h := range src {
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.
func collectDNSAddresses() (addrs []string, err error) {
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 {
addrs, err = appendDNSAddrsWithIfaces(addrs, hosts)
if err != nil {
diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go
index 7df8d320..787fb5d1 100644
--- a/internal/home/controlinstall.go
+++ b/internal/home/controlinstall.go
@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
- "net"
"net/http"
+ "net/netip"
"os"
"os/exec"
"path/filepath"
@@ -64,9 +64,9 @@ func (web *Web) handleInstallGetAddresses(w http.ResponseWriter, r *http.Request
}
type checkConfReqEnt struct {
- IP net.IP `json:"ip"`
- Port int `json:"port"`
- Autofix bool `json:"autofix"`
+ IP netip.Addr `json:"ip"`
+ Port int `json:"port"`
+ Autofix bool `json:"autofix"`
}
type checkConfReq struct {
@@ -117,7 +117,7 @@ func (req *checkConfReq) validateWeb(tcpPorts aghalg.UniqChecker[tcpPort]) (err
// 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
@@ -142,13 +142,13 @@ func (req *checkConfReq) validateDNS(
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 {
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) {
return false, err
}
@@ -160,7 +160,7 @@ func (req *checkConfReq) validateDNS(
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
}
@@ -196,7 +196,7 @@ func (web *Web) handleInstallCheckConfig(w http.ResponseWriter, r *http.Request)
// handleStaticIP - handles static IP request
// It either checks if we have a static IP
// 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{}
interfaceName := aghnet.InterfaceByIP(ip)
@@ -304,8 +304,8 @@ func disableDNSStubListener() error {
}
type applyConfigReqEnt struct {
- IP net.IP `json:"ip"`
- Port int `json:"port"`
+ IP netip.Addr `json:"ip"`
+ Port int `json:"port"`
}
type applyConfigReq struct {
@@ -397,14 +397,14 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
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 {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
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 {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -417,7 +417,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
Context.firstRun = false
config.BindHost = req.Web.IP
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
// TODO(e.burkov): StartMods() should be put in a separate goroutine at the
@@ -490,9 +490,9 @@ func decodeApplyConfigReq(r io.Reader) (req *applyConfigReq, restartHTTP bool, e
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 {
- 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 {
return nil, false, fmt.Errorf(
"checking address %s:%d: %w",
@@ -518,9 +518,9 @@ func (web *Web) registerInstallHandlers() {
// TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default checkConfigReqEnt.
type checkConfigReqEntBeta struct {
- IP []net.IP `json:"ip"`
- Port int `json:"port"`
- Autofix bool `json:"autofix"`
+ IP []netip.Addr `json:"ip"`
+ Port int `json:"port"`
+ Autofix bool `json:"autofix"`
}
// checkConfigReqBeta is a struct representing new client's config check request
@@ -590,8 +590,8 @@ func (web *Web) handleInstallCheckConfigBeta(w http.ResponseWriter, r *http.Requ
// TODO(e.burkov): This should removed with the API v1 when the appropriate
// functionality will appear in default applyConfigReqEnt.
type applyConfigReqEntBeta struct {
- IP []net.IP `json:"ip"`
- Port int `json:"port"`
+ IP []netip.Addr `json:"ip"`
+ Port int `json:"port"`
}
// applyConfigReqBeta is a struct representing new client's config setting
diff --git a/internal/home/dns.go b/internal/home/dns.go
index da462876..6c0d6531 100644
--- a/internal/home/dns.go
+++ b/internal/home/dns.go
@@ -3,6 +3,7 @@ package home
import (
"fmt"
"net"
+ "net/netip"
"net/url"
"os"
"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 {
return nil
}
- tcpAddrs = make([]*net.TCPAddr, len(ips))
- for i, ip := range ips {
- tcpAddrs[i] = &net.TCPAddr{
- IP: ip,
- Port: port,
- }
+ tcpAddrs = make([]*net.TCPAddr, 0, len(ips))
+ for _, ip := range ips {
+ tcpAddrs = append(tcpAddrs, net.TCPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
}
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 {
return nil
}
- udpAddrs = make([]*net.UDPAddr, len(ips))
- for i, ip := range ips {
- udpAddrs[i] = &net.UDPAddr{
- IP: ip,
- Port: port,
- }
+ udpAddrs = make([]*net.UDPAddr, 0, len(ips))
+ for _, ip := range ips {
+ udpAddrs = append(udpAddrs, net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, uint16(port))))
}
return udpAddrs
@@ -200,7 +195,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
dnsConf := config.DNS
hosts := dnsConf.BindHosts
if len(hosts) == 0 {
- hosts = []net.IP{{127, 0, 0, 1}}
+ hosts = []netip.Addr{aghnet.IPv4Localhost()}
}
newConf = dnsforward.ServerConfig{
@@ -257,7 +252,7 @@ func generateServerConfig() (newConf dnsforward.ServerConfig, err error) {
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 == "" {
return dnscc, errors.Error("no dnscrypt_config_file")
}
diff --git a/internal/home/home.go b/internal/home/home.go
index 289c1c64..adbf7bfb 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"net/http/pprof"
+ "net/netip"
"net/url"
"os"
"os/signal"
@@ -356,7 +357,7 @@ func setupConfig(opts options) (err error) {
}
// override bind host/port from the console
- if opts.bindHost != nil {
+ if opts.bindHost.IsValid() {
config.BindHost = opts.bindHost
}
if len(opts.pidFile) != 0 && writePIDFile(opts.pidFile) {
@@ -556,7 +557,7 @@ func checkPermissions() {
}
// 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 errors.Is(err, os.ErrPermission) {
log.Fatal(`Permission check failed.
diff --git a/internal/home/mobileconfig_test.go b/internal/home/mobileconfig_test.go
index 5230a2ac..48783d0d 100644
--- a/internal/home/mobileconfig_test.go
+++ b/internal/home/mobileconfig_test.go
@@ -3,12 +3,11 @@ package home
import (
"bytes"
"encoding/json"
- "net"
"net/http"
"net/http/httptest"
+ "net/netip"
"testing"
- "github.com/AdguardTeam/golibs/netutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"howett.net/plist"
@@ -28,7 +27,7 @@ func setupDNSIPs(t testing.TB) {
config = &configuration{
DNS: dnsConfig{
- BindHosts: []net.IP{netutil.IPv4Zero()},
+ BindHosts: []netip.Addr{netip.IPv4Unspecified()},
Port: defaultPortDNS,
},
}
diff --git a/internal/home/options.go b/internal/home/options.go
index 531a0fd4..e14e26f6 100644
--- a/internal/home/options.go
+++ b/internal/home/options.go
@@ -2,7 +2,7 @@ package home
import (
"fmt"
- "net"
+ "net/netip"
"os"
"strconv"
"strings"
@@ -35,7 +35,7 @@ type options struct {
serviceControlAction string
// bindHost is the address on which to serve the HTTP UI.
- bindHost net.IP
+ bindHost netip.Addr
// bindPort is the port on which to serve the HTTP UI.
bindPort int
@@ -130,14 +130,15 @@ var cmdLineOpts = []cmdLineOpt{{
longName: "work-dir",
shortName: "w",
}, {
- updateWithValue: func(o options, v string) (options, error) {
- o.bindHost = net.ParseIP(v)
- return o, nil
+ 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 == nil {
+ if !o.bindHost.IsValid() {
return "", false
}
diff --git a/internal/home/options_test.go b/internal/home/options_test.go
index 7954c0e4..32b4243a 100644
--- a/internal/home/options_test.go
+++ b/internal/home/options_test.go
@@ -2,7 +2,7 @@ package home
import (
"fmt"
- "net"
+ "net/netip"
"testing"
"github.com/stretchr/testify/assert"
@@ -56,11 +56,13 @@ func TestParseWorkDir(t *testing.T) {
}
func TestParseBindHost(t *testing.T) {
- assert.Nil(t, testParseOK(t).bindHost, "empty is not host")
- assert.Equal(t, net.IPv4(1, 2, 3, 4), testParseOK(t, "-h", "1.2.3.4").bindHost, "-h is host")
+ wantAddr := netip.MustParseAddr("1.2.3.4")
+
+ 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")
- 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")
}
@@ -149,8 +151,8 @@ func TestOptsToArgs(t *testing.T) {
opts: options{workDir: "path"},
}, {
name: "bind_host",
+ opts: options{bindHost: netip.MustParseAddr("1.2.3.4")},
args: []string{"-h", "1.2.3.4"},
- opts: options{bindHost: net.IP{1, 2, 3, 4}},
}, {
name: "bind_port",
args: []string{"-p", "666"},
diff --git a/internal/home/web.go b/internal/home/web.go
index 3e248d80..3cd04630 100644
--- a/internal/home/web.go
+++ b/internal/home/web.go
@@ -4,8 +4,8 @@ import (
"context"
"crypto/tls"
"io/fs"
- "net"
"net/http"
+ "net/netip"
"sync"
"time"
@@ -37,7 +37,7 @@ type webConfig struct {
clientFS fs.FS
clientBetaFS fs.FS
- BindHost net.IP
+ BindHost netip.Addr
BindPort int
BetaBindPort int
PortHTTPS int
@@ -135,8 +135,11 @@ func newWeb(conf *webConfig) (w *Web) {
//
// TODO(a.garipov): Adapt for HTTP/3.
func webCheckPortAvailable(port int) (ok bool) {
- return Context.web.httpsServer.server != nil ||
- aghnet.CheckPort("tcp", config.BindHost, port) == nil
+ if Context.web.httpsServer.server != nil {
+ return true
+ }
+
+ return aghnet.CheckPort("tcp", netip.AddrPortFrom(config.BindHost, uint16(port))) == nil
}
// TLSConfigChanged updates the TLS configuration and restarts the HTTPS server
From a1acfbbae4c222a6edecc705b2d2344d77a9f2be Mon Sep 17 00:00:00 2001
From: Ainar Garipov
Date: Fri, 14 Oct 2022 19:03:03 +0300
Subject: [PATCH 19/20] Pull request: 4925-refactor-tls-vol-1
Merge in DNS/adguard-home from 4925-refactor-tls-vol-1 to master
Squashed commit of the following:
commit ad87b2e93183b28f2e38666cc4267fa8dfd1cca0
Author: Ainar Garipov
Date: Fri Oct 14 18:49:22 2022 +0300
all: refactor tls, vol. 1
Co-Authored-By: Rahul Somasundaram
---
internal/aghtls/aghtls.go | 43 +++++++++++++-
internal/aghtls/aghtls_test.go | 57 +++++++++++++++++++
internal/aghtls/root.go | 14 +++++
internal/aghtls/root_linux.go | 56 ++++++++++++++++++
internal/aghtls/root_others.go | 9 +++
internal/home/home.go | 2 +-
internal/home/tls.go | 45 ---------------
.../{control_test.go => tls_internal_test.go} | 0
8 files changed, 179 insertions(+), 47 deletions(-)
create mode 100644 internal/aghtls/aghtls_test.go
create mode 100644 internal/aghtls/root.go
create mode 100644 internal/aghtls/root_linux.go
create mode 100644 internal/aghtls/root_others.go
rename internal/home/{control_test.go => tls_internal_test.go} (100%)
diff --git a/internal/aghtls/aghtls.go b/internal/aghtls/aghtls.go
index 5dc7a382..bcceaad9 100644
--- a/internal/aghtls/aghtls.go
+++ b/internal/aghtls/aghtls.go
@@ -1,7 +1,48 @@
// Package aghtls contains utilities for work with TLS.
package aghtls
-import "crypto/tls"
+import (
+ "crypto/tls"
+ "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
// weak cipher suites removed.
diff --git a/internal/aghtls/aghtls_test.go b/internal/aghtls/aghtls_test.go
new file mode 100644
index 00000000..923ff063
--- /dev/null
+++ b/internal/aghtls/aghtls_test.go
@@ -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)
+ })
+ }
+}
diff --git a/internal/aghtls/root.go b/internal/aghtls/root.go
new file mode 100644
index 00000000..d81db143
--- /dev/null
+++ b/internal/aghtls/root.go
@@ -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()
+}
diff --git a/internal/aghtls/root_linux.go b/internal/aghtls/root_linux.go
new file mode 100644
index 00000000..0805f198
--- /dev/null
+++ b/internal/aghtls/root_linux.go
@@ -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
+}
diff --git a/internal/aghtls/root_others.go b/internal/aghtls/root_others.go
new file mode 100644
index 00000000..38a50630
--- /dev/null
+++ b/internal/aghtls/root_others.go
@@ -0,0 +1,9 @@
+//go:build !linux
+
+package aghtls
+
+import "crypto/x509"
+
+func rootCAs() (roots *x509.CertPool) {
+ return nil
+}
diff --git a/internal/home/home.go b/internal/home/home.go
index adbf7bfb..dbe34f4e 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -147,7 +147,7 @@ func setupContext(opts options) {
// Go on.
}
- Context.tlsRoots = LoadSystemRootCAs()
+ Context.tlsRoots = aghtls.SystemRootCAs()
Context.transport = &http.Transport{
DialContext: customDialContext,
Proxy: getHTTPProxy,
diff --git a/internal/home/tls.go b/internal/home/tls.go
index a5089bd8..e7e8e35f 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -14,8 +14,6 @@ import (
"fmt"
"net/http"
"os"
- "path/filepath"
- "runtime"
"strings"
"sync"
"time"
@@ -699,46 +697,3 @@ func (t *TLSMod) registerWebHandlers() {
httpRegister(http.MethodPost, "/control/tls/configure", t.handleTLSConfigure)
httpRegister(http.MethodPost, "/control/tls/validate", t.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
-}
diff --git a/internal/home/control_test.go b/internal/home/tls_internal_test.go
similarity index 100%
rename from internal/home/control_test.go
rename to internal/home/tls_internal_test.go
From fee81b31eca6b1593aad6fe2c2780b0531998dec Mon Sep 17 00:00:00 2001
From: Ainar Garipov
Date: Fri, 14 Oct 2022 19:37:14 +0300
Subject: [PATCH 20/20] Pull request: 4925-refactor-tls-vol-2
Updates #4925.
Squashed commit of the following:
commit 4b221936ea6c2a244c404e95fa2a033571e07168
Author: Ainar Garipov
Date: Fri Oct 14 19:03:42 2022 +0300
all: refactor tls
---
internal/home/controlinstall.go | 2 +-
internal/home/home.go | 24 +-
internal/home/mobileconfig_test.go | 6 +-
internal/home/tls.go | 627 ++++++++++++++++-------------
internal/home/tls_internal_test.go | 59 +--
5 files changed, 399 insertions(+), 319 deletions(-)
diff --git a/internal/home/controlinstall.go b/internal/home/controlinstall.go
index 787fb5d1..0cbddf00 100644
--- a/internal/home/controlinstall.go
+++ b/internal/home/controlinstall.go
@@ -424,7 +424,7 @@ func (web *Web) handleInstallConfigure(w http.ResponseWriter, r *http.Request) {
// moment we'll allow setting up TLS in the initial configuration or the
// configuration itself will use HTTPS protocol, because the underlying
// functions potentially restart the HTTPS server.
- err = StartMods()
+ err = startMods()
if err != nil {
Context.firstRun = true
copyInstallSettings(config, curConfig)
diff --git a/internal/home/home.go b/internal/home/home.go
index dbe34f4e..70697a12 100644
--- a/internal/home/home.go
+++ b/internal/home/home.go
@@ -59,7 +59,7 @@ type homeContext struct {
auth *Auth // HTTP authentication module
filters *filtering.DNSFilter // DNS filtering 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
// (e.g. /etc/hosts) files.
etcHosts *aghnet.HostsContainer
@@ -117,7 +117,7 @@ func Main(clientBuildFS fs.FS) {
switch sig {
case syscall.SIGHUP:
Context.clients.Reload()
- Context.tls.Reload()
+ Context.tls.reload()
default:
cleanup(context.Background())
@@ -495,9 +495,9 @@ func run(opts options, clientBuildFS fs.FS) {
}
config.Users = nil
- Context.tls = tlsCreate(config.TLS)
- if Context.tls == nil {
- log.Fatalf("Can't initialize TLS module")
+ Context.tls, err = newTLSManager(config.TLS)
+ if err != nil {
+ log.Fatalf("initializing tls: %s", err)
}
Context.web, err = initWeb(opts, clientBuildFS)
@@ -507,7 +507,7 @@ func run(opts options, clientBuildFS fs.FS) {
err = initDNSServer()
fatalOnError(err)
- Context.tls.Start()
+ Context.tls.start()
go func() {
serr := startDNSServer()
@@ -531,20 +531,22 @@ func run(opts options, clientBuildFS fs.FS) {
select {}
}
-// StartMods initializes and starts the DNS server after installation.
-func StartMods() error {
+// startMods initializes and starts the DNS server after installation.
+func startMods() error {
err := initDNSServer()
if err != nil {
return err
}
- Context.tls.Start()
+ Context.tls.start()
err = startDNSServer()
if err != nil {
closeDNSServer()
+
return err
}
+
return nil
}
@@ -728,7 +730,6 @@ func cleanup(ctx context.Context) {
}
if Context.tls != nil {
- Context.tls.Close()
Context.tls = nil
}
}
@@ -738,7 +739,8 @@ func cleanupAlways() {
if len(Context.pidFileName) != 0 {
_ = os.Remove(Context.pidFileName)
}
- log.Info("Stopped")
+
+ log.Info("stopped")
}
func exitWithError() {
diff --git a/internal/home/mobileconfig_test.go b/internal/home/mobileconfig_test.go
index 48783d0d..3587154f 100644
--- a/internal/home/mobileconfig_test.go
+++ b/internal/home/mobileconfig_test.go
@@ -32,7 +32,7 @@ func setupDNSIPs(t testing.TB) {
},
}
- Context.tls = &TLSMod{}
+ Context.tls = &tlsManager{}
}
func TestHandleMobileConfigDoH(t *testing.T) {
@@ -65,7 +65,7 @@ func TestHandleMobileConfigDoH(t *testing.T) {
oldTLSConf := Context.tls
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)
require.NoError(t, err)
@@ -137,7 +137,7 @@ func TestHandleMobileConfigDoT(t *testing.T) {
oldTLSConf := Context.tls
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)
require.NoError(t, err)
diff --git a/internal/home/tls.go b/internal/home/tls.go
index e7e8e35f..55c58fb5 100644
--- a/internal/home/tls.go
+++ b/internal/home/tls.go
@@ -26,216 +26,256 @@ import (
"github.com/google/go-cmp/cmp"
)
-var tlsWebHandlersRegistered = false
+// tlsManager contains the current configuration and state of AdGuard Home TLS
+// encryption.
+type tlsManager struct {
+ // status is the current status of the configuration. It is never nil.
+ status *tlsConfigStatus
-// TLSMod - TLS module object
-type TLSMod struct {
- certLastMod time.Time // last modification time of the certificate file
- status tlsConfigStatus
- confLock sync.Mutex
- conf tlsConfigSettings
+ // certLastMod is the last modification time of the certificate file.
+ certLastMod time.Time
+
+ confLock sync.Mutex
+ conf tlsConfigSettings
}
-// Create TLS module
-func tlsCreate(conf tlsConfigSettings) *TLSMod {
- t := &TLSMod{}
- t.conf = conf
- if t.conf.Enabled {
- 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,
- }}
+// newTLSManager initializes the TLS configuration.
+func newTLSManager(conf tlsConfigSettings) (m *tlsManager, err error) {
+ m = &tlsManager{
+ status: &tlsConfigStatus{},
+ conf: conf,
+ }
+
+ if m.conf.Enabled {
+ err = m.load()
+ if err != nil {
+ return nil, err
}
- t.setCertFileTime()
+
+ m.setCertFileTime()
}
- return t
+
+ return m, nil
}
-func (t *TLSMod) load() bool {
- if !tlsLoadConfig(&t.conf, &t.status) {
- log.Error("failed to load TLS config: %s", t.status.WarningValidation)
- return false
+// load reloads the TLS configuration from files or data from the config file.
+func (m *tlsManager) load() (err error) {
+ err = loadTLSConf(&m.conf, m.status)
+ if err != nil {
+ return fmt.Errorf("loading config: %w", err)
}
- // validate current TLS config and update warnings (it could have been loaded from file)
- 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() {
+ return nil
}
// WriteDiskConfig - write config
-func (t *TLSMod) WriteDiskConfig(conf *tlsConfigSettings) {
- t.confLock.Lock()
- *conf = t.conf
- t.confLock.Unlock()
+func (m *tlsManager) WriteDiskConfig(conf *tlsConfigSettings) {
+ m.confLock.Lock()
+ *conf = m.conf
+ m.confLock.Unlock()
}
-func (t *TLSMod) setCertFileTime() {
- if len(t.conf.CertificatePath) == 0 {
+// setCertFileTime sets t.certLastMod from the certificate. If there are
+// errors, setCertFileTime logs them.
+func (m *tlsManager) setCertFileTime() {
+ if len(m.conf.CertificatePath) == 0 {
return
}
- fi, err := os.Stat(t.conf.CertificatePath)
+
+ fi, err := os.Stat(m.conf.CertificatePath)
if err != nil {
- log.Error("TLS: %s", err)
+ log.Error("tls: looking up certificate path: %s", err)
+
return
}
- t.certLastMod = fi.ModTime().UTC()
+
+ m.certLastMod = fi.ModTime().UTC()
}
-// Start updates the configuration of TLSMod and starts it.
-func (t *TLSMod) Start() {
- if !tlsWebHandlersRegistered {
- tlsWebHandlersRegistered = true
- t.registerWebHandlers()
- }
+// start updates the configuration of t and starts it.
+func (m *tlsManager) start() {
+ m.registerWebHandlers()
- t.confLock.Lock()
- tlsConf := t.conf
- t.confLock.Unlock()
+ m.confLock.Lock()
+ tlsConf := m.conf
+ m.confLock.Unlock()
- // The background context is used because the TLSConfigChanged wraps
- // context with timeout on its own and shuts down the server, which
- // handles current request.
+ // The background context is used because the TLSConfigChanged wraps context
+ // with timeout on its own and shuts down the server, which handles current
+ // request.
Context.web.TLSConfigChanged(context.Background(), tlsConf)
}
-// Reload updates the configuration of TLSMod and restarts it.
-func (t *TLSMod) Reload() {
- t.confLock.Lock()
- tlsConf := t.conf
- t.confLock.Unlock()
+// reload updates the configuration and restarts t.
+func (m *tlsManager) reload() {
+ m.confLock.Lock()
+ tlsConf := m.conf
+ m.confLock.Unlock()
if !tlsConf.Enabled || len(tlsConf.CertificatePath) == 0 {
return
}
+
fi, err := os.Stat(tlsConf.CertificatePath)
if err != nil {
- 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")
+ log.Error("tls: %s", err)
- t.confLock.Lock()
- r := t.load()
- t.confLock.Unlock()
- if !r {
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()
- t.confLock.Lock()
- tlsConf = t.conf
- t.confLock.Unlock()
- // The background context is used because the TLSConfigChanged wraps
- // context with timeout on its own and shuts down the server, which
- // handles current request.
+ m.confLock.Lock()
+ tlsConf = m.conf
+ m.confLock.Unlock()
+
+ // The background context is used because the TLSConfigChanged wraps context
+ // with timeout on its own and shuts down the server, which handles current
+ // request.
Context.web.TLSConfigChanged(context.Background(), tlsConf)
}
-// Set certificate and private key data
-func tlsLoadConfig(tls *tlsConfigSettings, status *tlsConfigStatus) bool {
- tls.CertificateChainData = []byte(tls.CertificateChain)
- tls.PrivateKeyData = []byte(tls.PrivateKey)
-
- 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)
+// loadTLSConf loads and validates the TLS configuration. The returned error is
+// also set in status.WarningValidation.
+func loadTLSConf(tlsConf *tlsConfigSettings, status *tlsConfigStatus) (err error) {
+ defer func() {
if err != nil {
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
}
- if tls.PrivateKeyPath != "" {
- if tls.PrivateKey != "" {
- status.WarningValidation = "private key data and file can't be set together"
- return false
+ if tlsConf.PrivateKeyPath != "" {
+ if tlsConf.PrivateKey != "" {
+ return errors.Error("private key data and file can't be set together")
}
- tls.PrivateKeyData, err = os.ReadFile(tls.PrivateKeyPath)
+
+ tlsConf.PrivateKeyData, err = os.ReadFile(tlsConf.PrivateKeyPath)
if err != nil {
- status.WarningValidation = err.Error()
- return false
+ return fmt.Errorf("reading key file: %w", err)
}
+
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 {
- ValidCert bool `json:"valid_cert"` // ValidCert is true if the specified certificates chain is a valid chain of X509 certificates
- 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 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
+ // Subject is the subject of the first certificate in the chain.
+ Subject string `json:"subject,omitempty"`
- // key status
- ValidKey bool `json:"valid_key"` // ValidKey is true if the key is a valid private key
- KeyType string `json:"key_type,omitempty"` // KeyType is one of RSA or ECDSA
+ // Issuer is the issuer of the first certificate in the chain.
+ Issuer string `json:"issuer,omitempty"`
- // is usable? set by validator
- ValidPair bool `json:"valid_pair"` // ValidPair is true if both certificate and private key are correct
+ // KeyType is the type of the private key.
+ KeyType string `json:"key_type,omitempty"`
- // warnings
- WarningValidation string `json:"warning_validation,omitempty"` // WarningValidation is a validation warning message with the issue description
+ // NotBefore is the NotBefore field of the first certificate in the chain.
+ 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 {
- tlsConfigStatus `json:",inline"`
+ *tlsConfigStatus `json:",inline"`
tlsConfigSettingsExt `json:",inline"`
}
-// tlsConfigSettingsExt is used to (un)marshal PrivateKeySaved to ensure that
-// clients don't send and receive previously saved private keys.
+// tlsConfigSettingsExt is used to (un)marshal the PrivateKeySaved field to
+// ensure that clients don't send and receive previously saved private keys.
type tlsConfigSettingsExt struct {
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"`
}
-func (t *TLSMod) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
- t.confLock.Lock()
+func (m *tlsManager) handleTLSStatus(w http.ResponseWriter, r *http.Request) {
+ m.confLock.Lock()
data := tlsConfig{
tlsConfigSettingsExt: tlsConfigSettingsExt{
- tlsConfigSettings: t.conf,
+ tlsConfigSettings: m.conf,
},
- tlsConfigStatus: t.status,
+ tlsConfigStatus: m.status,
}
- t.confLock.Unlock()
+ m.confLock.Unlock()
+
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)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
@@ -244,7 +284,7 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
}
if setts.PrivateKeySaved {
- setts.PrivateKey = t.conf.PrivateKey
+ setts.PrivateKey = m.conf.PrivateKey
}
if setts.Enabled {
@@ -276,75 +316,74 @@ func (t *TLSMod) handleTLSValidate(w http.ResponseWriter, r *http.Request) {
return
}
- status := tlsConfigStatus{}
- if tlsLoadConfig(&setts.tlsConfigSettings, &status) {
- status = validateCertificates(string(setts.CertificateChainData), string(setts.PrivateKeyData), setts.ServerName)
- }
-
- data := tlsConfig{
+ // Skip the error check, since we are only interested in the value of
+ // status.WarningValidation.
+ status := &tlsConfigStatus{}
+ _ = loadTLSConf(&setts.tlsConfigSettings, status)
+ resp := tlsConfig{
tlsConfigSettingsExt: setts,
tlsConfigStatus: status,
}
- marshalTLS(w, r, data)
+ marshalTLS(w, r, resp)
}
-func (t *TLSMod) setConfig(newConf tlsConfigSettings, status tlsConfigStatus) (restartHTTPS bool) {
- t.confLock.Lock()
- defer t.confLock.Unlock()
+func (m *tlsManager) setConfig(newConf tlsConfigSettings, status *tlsConfigStatus) (restartHTTPS bool) {
+ m.confLock.Lock()
+ defer m.confLock.Unlock()
// Reset the DNSCrypt data before comparing, since we currently do not
// accept these from the frontend.
//
// TODO(a.garipov): Define a custom comparer for dnsforward.TLSConfig.
- newConf.DNSCryptConfigFile = t.conf.DNSCryptConfigFile
- newConf.PortDNSCrypt = t.conf.PortDNSCrypt
- if !cmp.Equal(t.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
+ newConf.DNSCryptConfigFile = m.conf.DNSCryptConfigFile
+ newConf.PortDNSCrypt = m.conf.PortDNSCrypt
+ if !cmp.Equal(m.conf, newConf, cmp.AllowUnexported(dnsforward.TLSConfig{})) {
log.Info("tls config has changed, restarting https server")
restartHTTPS = true
} 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
- t.conf.Enabled = newConf.Enabled
- t.conf.ServerName = newConf.ServerName
- t.conf.ForceHTTPS = newConf.ForceHTTPS
- t.conf.PortHTTPS = newConf.PortHTTPS
- t.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
- t.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
- t.conf.CertificateChain = newConf.CertificateChain
- t.conf.CertificatePath = newConf.CertificatePath
- t.conf.CertificateChainData = newConf.CertificateChainData
- t.conf.PrivateKey = newConf.PrivateKey
- t.conf.PrivateKeyPath = newConf.PrivateKeyPath
- t.conf.PrivateKeyData = newConf.PrivateKeyData
- t.status = status
+ m.conf.Enabled = newConf.Enabled
+ m.conf.ServerName = newConf.ServerName
+ m.conf.ForceHTTPS = newConf.ForceHTTPS
+ m.conf.PortHTTPS = newConf.PortHTTPS
+ m.conf.PortDNSOverTLS = newConf.PortDNSOverTLS
+ m.conf.PortDNSOverQUIC = newConf.PortDNSOverQUIC
+ m.conf.CertificateChain = newConf.CertificateChain
+ m.conf.CertificatePath = newConf.CertificatePath
+ m.conf.CertificateChainData = newConf.CertificateChainData
+ m.conf.PrivateKey = newConf.PrivateKey
+ m.conf.PrivateKeyPath = newConf.PrivateKeyPath
+ m.conf.PrivateKeyData = newConf.PrivateKeyData
+ m.status = status
return restartHTTPS
}
-func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
- data, err := unmarshalTLS(r)
+func (m *tlsManager) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
+ req, err := unmarshalTLS(r)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "Failed to unmarshal TLS config: %s", err)
return
}
- if data.PrivateKeySaved {
- data.PrivateKey = t.conf.PrivateKey
+ if req.PrivateKeySaved {
+ req.PrivateKey = m.conf.PrivateKey
}
- if data.Enabled {
+ if req.Enabled {
err = validatePorts(
tcpPort(config.BindPort),
tcpPort(config.BetaBindPort),
- tcpPort(data.PortHTTPS),
- tcpPort(data.PortDNSOverTLS),
- tcpPort(data.PortDNSCrypt),
+ tcpPort(req.PortHTTPS),
+ tcpPort(req.PortDNSOverTLS),
+ tcpPort(req.PortDNSCrypt),
udpPort(config.DNS.Port),
- udpPort(data.PortDNSOverQUIC),
+ udpPort(req.PortDNSOverQUIC),
)
if err != nil {
aghhttp.Error(r, w, http.StatusBadRequest, "%s", err)
@@ -354,33 +393,33 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
}
// TODO(e.burkov): Investigate and perhaps check other ports.
- if !webCheckPortAvailable(data.PortHTTPS) {
+ if !webCheckPortAvailable(req.PortHTTPS) {
aghhttp.Error(
r,
w,
http.StatusBadRequest,
- "port %d is not available, cannot enable HTTPS on it",
- data.PortHTTPS,
+ "port %d is not available, cannot enable https on it",
+ req.PortHTTPS,
)
return
}
- status := tlsConfigStatus{}
- if !tlsLoadConfig(&data.tlsConfigSettings, &status) {
- data2 := tlsConfig{
- tlsConfigSettingsExt: data,
- tlsConfigStatus: t.status,
+ status := &tlsConfigStatus{}
+ err = loadTLSConf(&req.tlsConfigSettings, status)
+ if err != nil {
+ resp := tlsConfig{
+ tlsConfigSettingsExt: req,
+ tlsConfigStatus: status,
}
- marshalTLS(w, r, data2)
+
+ marshalTLS(w, r, resp)
return
}
- status = validateCertificates(string(data.CertificateChainData), string(data.PrivateKeyData), data.ServerName)
-
- restartHTTPS := t.setConfig(data.tlsConfigSettings, status)
- t.setCertFileTime()
+ restartHTTPS := m.setConfig(req.tlsConfigSettings, status)
+ m.setCertFileTime()
onConfigModified()
err = reconfigureDNSServer()
@@ -390,12 +429,12 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
return
}
- data2 := tlsConfig{
- tlsConfigSettingsExt: data,
- tlsConfigStatus: t.status,
+ resp := tlsConfig{
+ tlsConfigSettingsExt: req,
+ tlsConfigStatus: m.status,
}
- marshalTLS(w, r, data2)
+ marshalTLS(w, r, resp)
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
@@ -406,7 +445,7 @@ func (t *TLSMod) handleTLSConfigure(w http.ResponseWriter, r *http.Request) {
// same reason.
if restartHTTPS {
go func() {
- Context.web.TLSConfigChanged(context.Background(), data.tlsConfigSettings)
+ Context.web.TLSConfigChanged(context.Background(), req.tlsConfigSettings)
}()
}
}
@@ -443,89 +482,105 @@ func validatePorts(
return nil
}
-func verifyCertChain(data *tlsConfigStatus, certChain, serverName string) error {
- log.Tracef("TLS: got certificate: %d bytes", len(certChain))
+// validateCertChain validates the certificate chain and sets data in status.
+// 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
- var certs []*pem.Block // PEM-encoded certificates
+ log.Debug("tls: got certificate chain: %d bytes", len(certChain))
- pemblock := []byte(certChain)
+ var certs []*pem.Block
+ pemblock := certChain
for {
var decoded *pem.Block
decoded, pemblock = pem.Decode(pemblock)
if decoded == nil {
break
}
+
if decoded.Type == "CERTIFICATE" {
certs = append(certs, decoded)
}
}
- var parsedCerts []*x509.Certificate
-
- for _, cert := range certs {
- parsed, err := x509.ParseCertificate(cert.Bytes)
- if err != nil {
- data.WarningValidation = fmt.Sprintf("Failed to parse certificate: %s", err)
- return errors.Error(data.WarningValidation)
- }
- parsedCerts = append(parsedCerts, parsed)
+ parsedCerts, err := parsePEMCerts(certs)
+ if err != nil {
+ return err
}
- if len(parsedCerts) == 0 {
- data.WarningValidation = "You have specified an empty certificate"
- return errors.Error(data.WarningValidation)
- }
-
- data.ValidCert = true
-
- // spew.Dump(parsedCerts)
+ status.ValidCert = true
opts := x509.VerifyOptions{
DNSName: serverName,
Roots: Context.tlsRoots,
}
- log.Printf("number of certs - %d", len(parsedCerts))
- if len(parsedCerts) > 1 {
- // set up an intermediate
- pool := x509.NewCertPool()
- for _, cert := range parsedCerts[1:] {
- log.Printf("got an intermediate cert")
- pool.AddCert(cert)
- }
- opts.Intermediates = pool
+ log.Info("tls: number of certs: %d", len(parsedCerts))
+
+ pool := x509.NewCertPool()
+ for _, cert := range parsedCerts[1:] {
+ log.Info("tls: got an intermediate cert")
+ pool.AddCert(cert)
}
- // TODO: save it as a warning rather than error it out -- shouldn't be a big problem
+ opts.Intermediates = pool
+
mainCert := parsedCerts[0]
- _, err := mainCert.Verify(opts)
+ _, err = mainCert.Verify(opts)
if err != nil {
- // let self-signed certs through
- data.WarningValidation = fmt.Sprintf("Your certificate does not verify: %s", err)
+ // Let self-signed certs through and don't return this error.
+ status.WarningValidation = fmt.Sprintf("certificate does not verify: %s", err)
} else {
- data.ValidChain = true
+ status.ValidChain = true
}
- // spew.Dump(chains)
- // update status
if mainCert != nil {
- notAfter := mainCert.NotAfter
- data.Subject = mainCert.Subject.String()
- data.Issuer = mainCert.Issuer.String()
- data.NotAfter = notAfter
- data.NotBefore = mainCert.NotBefore
- data.DNSNames = mainCert.DNSNames
+ status.Subject = mainCert.Subject.String()
+ status.Issuer = mainCert.Issuer.String()
+ status.NotAfter = mainCert.NotAfter
+ status.NotBefore = mainCert.NotBefore
+ status.DNSNames = mainCert.DNSNames
}
return nil
}
-func validatePkey(data *tlsConfigStatus, pkey string) error {
- // now do a more extended validation
- var key *pem.Block // PEM-encoded certificates
+// parsePEMCerts parses multiple PEM-encoded certificates.
+func parsePEMCerts(certs []*pem.Block) (parsedCerts []*x509.Certificate, err error) {
+ 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)
for {
var decoded *pem.Block
@@ -542,61 +597,77 @@ func validatePkey(data *tlsConfigStatus, pkey string) error {
}
if key == nil {
- data.WarningValidation = "No valid keys were found"
-
- return errors.Error(data.WarningValidation)
+ return errors.Error("no valid keys were found")
}
- // parse the decoded key
_, keyType, err := parsePrivateKey(key.Bytes)
if err != nil {
- data.WarningValidation = fmt.Sprintf("Failed to parse private key: %s", 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)
+ return fmt.Errorf("parsing private key: %w", err)
}
- data.ValidKey = true
- data.KeyType = keyType
+ if keyType == keyTypeED25519 {
+ 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
}
// validateCertificates processes certificate data and its private key. All
-// parameters are optional. On error, validateCertificates returns a partially
-// set object with field WarningValidation containing error description.
-func validateCertificates(certChain, pkey, serverName string) tlsConfigStatus {
- var data tlsConfigStatus
-
- // check only public certificate separately from the key
- if certChain != "" {
- if verifyCertChain(&data, certChain, serverName) != nil {
- return data
+// parameters are optional. status must not be nil. The returned error is also
+// set in status.WarningValidation.
+func validateCertificates(
+ 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:]
}
- }
+ }()
- // 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))
+ // Check only the public certificate separately from the key.
+ if len(certChain) > 0 {
+ err = validateCertChain(status, certChain, serverName)
if err != nil {
- data.WarningValidation = fmt.Sprintf("Invalid certificate or key: %s", err)
- return data
+ return err
}
- 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.
@@ -691,9 +762,9 @@ func marshalTLS(w http.ResponseWriter, r *http.Request, data tlsConfig) {
_ = aghhttp.WriteJSONResponse(w, r, data)
}
-// registerWebHandlers registers HTTP handlers for TLS configuration
-func (t *TLSMod) registerWebHandlers() {
- httpRegister(http.MethodGet, "/control/tls/status", t.handleTLSStatus)
- httpRegister(http.MethodPost, "/control/tls/configure", t.handleTLSConfigure)
- httpRegister(http.MethodPost, "/control/tls/validate", t.handleTLSValidate)
+// registerWebHandlers registers HTTP handlers for TLS configuration.
+func (m *tlsManager) registerWebHandlers() {
+ httpRegister(http.MethodGet, "/control/tls/status", m.handleTLSStatus)
+ httpRegister(http.MethodPost, "/control/tls/configure", m.handleTLSConfigure)
+ httpRegister(http.MethodPost, "/control/tls/validate", m.handleTLSValidate)
}
diff --git a/internal/home/tls_internal_test.go b/internal/home/tls_internal_test.go
index 46f14a2a..b6e02f24 100644
--- a/internal/home/tls_internal_test.go
+++ b/internal/home/tls_internal_test.go
@@ -7,8 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
-const (
- CertificateChain = `-----BEGIN CERTIFICATE-----
+var testCertChainData = []byte(`-----BEGIN CERTIFICATE-----
MIICKzCCAZSgAwIBAgIJAMT9kPVJdM7LMA0GCSqGSIb3DQEBCwUAMC0xFDASBgNV
BAoMC0FkR3VhcmQgTHRkMRUwEwYDVQQDDAxBZEd1YXJkIEhvbWUwHhcNMTkwMjI3
MDkyNDIzWhcNNDYwNzE0MDkyNDIzWjAtMRQwEgYDVQQKDAtBZEd1YXJkIEx0ZDEV
@@ -21,8 +20,9 @@ eKO029jYd2AAZEQwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOBgQB8
LwlXfbakf7qkVTlCNXgoY7RaJ8rJdPgOZPoCTVToEhT6u/cb1c2qp8QB0dNExDna
b0Z+dnODTZqQOJo6z/wIXlcUrnR4cQVvytXt8lFn+26l6Y6EMI26twC/xWr+1swq
Muj4FeWHVDerquH4yMr1jsYLD3ci+kc5sbIX6TfVxQ==
------END CERTIFICATE-----`
- PrivateKey = `-----BEGIN PRIVATE KEY-----
+-----END CERTIFICATE-----`)
+
+var testPrivateKeyData = []byte(`-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALC/BSc8mI68tw5p
aYa7pjrySwWvXeetcFywOWHGVfLw9qiFWLdfESa3Y6tWMpZAXD9t1Xh9n211YUBV
FGSB4ZshnM/tgEPU6t787lJD4NsIIRp++MkJxdAitN4oUTqL0bdpIwezQ/CrYuBX
@@ -37,36 +37,43 @@ An/jMjZSMCxNl6UyFcqt5Et1EGVhuFECQQCZLXxaT+qcyHjlHJTMzuMgkz1QFbEp
O5EX70gpeGQMPDK0QSWpaazg956njJSDbNCFM4BccrdQbJu1cW4qOsfBAkAMgZuG
O88slmgTRHX4JGFmy3rrLiHNI2BbJSuJ++Yllz8beVzh6NfvuY+HKRCmPqoBPATU
kXS9jgARhhiWXJrk
------END PRIVATE KEY-----`
-)
+-----END PRIVATE KEY-----`)
func TestValidateCertificates(t *testing.T) {
t.Run("bad_certificate", func(t *testing.T) {
- data := validateCertificates("bad cert", "", "")
- assert.NotEmpty(t, data.WarningValidation)
- assert.False(t, data.ValidCert)
- assert.False(t, data.ValidChain)
+ status := &tlsConfigStatus{}
+ err := validateCertificates(status, []byte("bad cert"), nil, "")
+ assert.Error(t, err)
+ assert.NotEmpty(t, status.WarningValidation)
+ assert.False(t, status.ValidCert)
+ assert.False(t, status.ValidChain)
})
t.Run("bad_private_key", func(t *testing.T) {
- data := validateCertificates("", "bad priv key", "")
- assert.NotEmpty(t, data.WarningValidation)
- assert.False(t, data.ValidKey)
+ status := &tlsConfigStatus{}
+ err := validateCertificates(status, nil, []byte("bad priv key"), "")
+ assert.Error(t, err)
+ assert.NotEmpty(t, status.WarningValidation)
+ assert.False(t, status.ValidKey)
})
t.Run("valid", func(t *testing.T) {
- data := validateCertificates(CertificateChain, PrivateKey, "")
- notBefore, _ := time.Parse(time.RFC3339, "2019-02-27T09:24:23Z")
- notAfter, _ := time.Parse(time.RFC3339, "2046-07-14T09:24:23Z")
- assert.NotEmpty(t, data.WarningValidation)
- assert.True(t, data.ValidCert)
- assert.False(t, data.ValidChain)
- assert.True(t, data.ValidKey)
- assert.Equal(t, "RSA", data.KeyType)
- assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Subject)
- assert.Equal(t, "CN=AdGuard Home,O=AdGuard Ltd", data.Issuer)
- assert.Equal(t, notBefore, data.NotBefore)
- assert.Equal(t, notAfter, data.NotAfter)
- assert.True(t, data.ValidPair)
+ status := &tlsConfigStatus{}
+ err := validateCertificates(status, testCertChainData, testPrivateKeyData, "")
+ assert.NoError(t, err)
+
+ notBefore := time.Date(2019, 2, 27, 9, 24, 23, 0, time.UTC)
+ notAfter := time.Date(2046, 7, 14, 9, 24, 23, 0, time.UTC)
+
+ assert.NotEmpty(t, status.WarningValidation)
+ assert.True(t, status.ValidCert)
+ assert.False(t, status.ValidChain)
+ assert.True(t, status.ValidKey)
+ assert.Equal(t, "RSA", status.KeyType)
+ 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)
})
}