LF78
October 27, 2021, 9:44am
1
Hi.
Is there any plan to implement a native support of DNS-over-HTTPS (DOH)? I tried to set-up Cloudflared on Docker with a VLAN and it seems to work but I am not so sure of my installation because it is sometimes “unhealthy” and my skills are too limited to fix it properly.
It would be a great simplification if there were a native support as on AdGuard Home.
Thanks.
I think this is still out-of-scope:
and
Yes and No.
Yes: The DNS server in FTL is based on dnsmasq and, as such, is a DNS proxy that can either reply from its internal cache or has to forward the request to somewhere else. In the case of DoT, pihole-FTL forwards the requests to a local proxy which itself forwards it to Cloudflare using an encrypted connection.
There are several complications when it comes to implementing this into pihole-FTL: There are different flavors of encrypted DNS traffic: DNS-over-TLS (DoT), DNS-over-HTTPS (…
DL6ER
July 12, 2026, 9:37am
4
Years later, we thinkgs are starting to move again and native encrypted upstreams are currently being worked on: we are adding DoT and DoH support directly into FTL, so Pi-hole can reach upstream resolvers over an encrypted connection without cloudflared or any other sidecar.
The client side is implemented and open for review:
development ← new/dot-doh-client
opened 10:22AM - 07 Jul 26 UTC
# What does this implement/fix?
Native **DoT** (DNS-over-TLS, [RFC 7858](https:… //www.rfc-editor.org/rfc/rfc7858.html)) and **DoH** (DNS-over-HTTPS, [RFC 8484](https://www.rfc-editor.org/rfc/rfc8484.html)) for FTL's **outbound** resolution - FTL talking to an encrypted upstream resolver - with **zero dnsmasq changes**.
**How it works.** dnsmasq cannot speak TLS to its upstreams, so FTL runs a small local forward-proxy. For every encrypted entry in `dns.upstreams`, FTL binds a loopback listener pair (UDP+TCP) in `127.47.11.0/24` and emits a `server=127.47.11.N#port` line into `dnsmasq.conf`. dnsmasq forwards the plaintext query to that listener as usual; a dedicated FTL worker thread then re-encrypts it, forwards it to the real resolver over DoT/DoH via mbedTLS, and hands the answer back. No query leaves the host in the clear.
**What is added** (new module `src/dotdoh/`, leaf units self-contained and unit-tested):
- `upstream_uri` - parser/classifier for `tls://`, `https://` and plaintext upstream entries.
- `framing` - DoT two-octet length prefix ([RFC 7858](https://www.rfc-editor.org/rfc/rfc7858.html)) and DoH HTTP/1.1 request/response ([RFC 8484](https://www.rfc-editor.org/rfc/rfc8484.html)). Bytes only; DNS message contents are never parsed here.
- `registry` - one loopback listener pair per upstream, bind-first (no `SO_REUSEADDR`, so nothing can co-bind).
- `tls_client` - fail-closed mbedTLS client: required chain + hostname verification, SNI, a pooled connection per upstream with a single reconnect. Never downgrades to plaintext.
- `edns_pad` - pads each outbound query to a 128-octet boundary with an EDNS(0) Padding option ([RFC 7830](https://www.rfc-editor.org/rfc/rfc7830.html) / [RFC 8467](https://www.rfc-editor.org/rfc/rfc8467.html)) so the ciphertext length no longer leaks the query size. Only the encrypted leg is padded. As a missing padding reduces obscuring the question length somewhat but does not weaken the integrity and/or encryption, this is implemented fail-open, so a query that cannot be padded is still sent rather than dropped.
- `proxy` - the forward-proxy worker thread, armed after dnsmasq startup so the listener fds are not reused.
**Config / API:**
- `dns.upstreams` now accepts `tls://` and `https://` URIs (validated by the parser).
- New `dns.upstreamCA` trust-anchor option (documented in the OpenAPI spec).
- The suggested-servers list gained v4/v6-**pinned** DoT/DoH URIs for the well-known resolvers (Google, Quad9, Cloudflare, OpenDNS), e.g. `tls://dns.google@8.8.8.8`. Pinning the address (`sni@ip`) avoids an untrusted bootstrap lookup of the resolver hostname before switching to it.
**Security posture:** encrypted upstreams are fail-closed - a failed certificate or hostname verification drops the answer rather than falling back to plaintext.
| Privacy feature | Shipped |
| --- | --- |
| CA trust-anchor chain verification - system trust store or a custom `dns.upstreamCA` bundle | :white_check_mark: |
| Server certificate hostname and SNI verification | :white_check_mark: |
| Strict privacy ([RFC 8310](https://www.rfc-editor.org/rfc/rfc8310.html)) - fail-closed, never downgrades to plaintext | :white_check_mark: |
| EDNS(0) query padding to a 128-octet boundary ([RFC 7830](https://www.rfc-editor.org/rfc/rfc7830.html) / [RFC 8467](https://www.rfc-editor.org/rfc/rfc8467.html)) | :white_check_mark: |
## How to test this locally
Set an encrypted upstream - via the web UI, a `PATCH /api/config`, or by editing `dns.upstreams` in `pihole.toml` - for example `tls://dns.google@8.8.8.8` (DoT) or `https://cloudflare-dns.com@1.1.1.1/dns-query` (DoH). FTL restarts, arms the proxy, and rewrites `dnsmasq.conf`. Then resolve normally; the query is carried to the upstream over TLS:
```
dig @127.0.0.1 example.com
```
FTL's log shows a `dotdoh: <DoT|DoH> upstream <name> armed` line for each encrypted upstream.
## How the CI tests this automatically
Two layers, both wired into the existing test job (`test/run.sh`):
1. **Unit / regression harness** - a standalone `dotdoh_regression` executable (same pattern as `tar_regression`/`gzip_regression`) exercises the URI parser and the DoT/DoH framing against a table of expected results. It is built in CI via `-DBUILD_DOTDOH_REGRESSION=ON` (`.github/Dockerfile` + `build.sh`) and run as a bats case in `test_suite.bats` ("DoT/DoH parser regression harness"). It can be built with ASan/UBSan locally via `./build.sh "-DDOTDOH_REGRESSION_SANITIZE=ON" test`.
2. **End-to-end** - `test/dotdoh.bats` configures a real encrypted upstream and resolves **real DoT and DoH** through a local TLS shim (`test/dotdoh_shim.py`, terminating TLS with the repo test certificate) into the recursor, asserting the decrypted answer. It runs **after** the pytest API suite on purpose: switching the upstream restarts FTL, which would otherwise perturb the query statistics those API tests assert on. `test_final.bats` still accounts for its config writes.
## Scope: part 1 of 2
This PR is **part 1 of 2** and deliberately covers **only the outbound direction** (FTL as a DoT/DoH *client* toward the upstream resolver).
**Part 2** - the **server** direction (FTL terminating inbound DoT/DoH from downstream clients and handing plaintext to dnsmasq) - will follow as a **separate** PR, submitted once this one is reviewed and merged, to keep each direction independently reviewable.
Much of the foundation laid here is deliberately reusable for part 2: the wire framing (`src/dotdoh/framing.c`), the URI/config plumbing, and the mbedTLS and test-shim scaffolding are shared between both directions - the module is named `dotdoh` (not `client`) for exactly this reason, so the server side can live beside it.
---
**Related issue or feature (if applicable):** N/A
**Pull request in [docs](https://github.com/pi-hole/docs) with documentation (if applicable):** N/A
---
**By submitting this pull request, I confirm the following:**
1. I have read and understood the [contributors guide](https://docs.pi-hole.net/guides/github/contributing/), as well as this entire template.
2. I have commented my proposed changes within the code.
3. I am willing to help maintain this change if there are issues with it later.
4. It is compatible with the [EUPL 1.2 license](https://opensource.org/licenses/EUPL-1.1)
5. I have squashed any insignificant commits.
## Checklist:
- [x] The code change is tested and works locally.
- [x] I based my code and PRs against the repositories `development` branch.
- [x] I signed off all commits (DCO).
- [x] I signed all my commits.
- [x] I have read the above and my PR is ready for review.
Roadmap (OpenSSL migration, HTTP/2, downstream DoT/DoH with FTL as the server, DoQ later):
development ← new/dot-doh-client
I have a series of changes I'd like to get reviewed/tested and merged in order:
…
1. DoH + DoT to upstream (FTL is client) - this PR
2. OpenSSL migration (#2941)
3. Opt-in experimental support for HTTP/2 (PR to be created, code already written)
4. DoH + DoT downstream to the clients (FTL is server). This is what you have asked for. The code is mostly written, PR to be opened.
5. DoQ client + server (code yet to be finished)
Once this all is tested and verified, we will probably aim for the next release. However, I ask for your patience as reviewing is a slow process so this won't land tonight or next week.
---
**edit** I started with the client feature as this is where encryption is probably more "needed". The upstream connection goes through the open Internet so encryption really brings confidentiality. Downstream to your clients happens in your local network where this isn't really needed, here the feature is more needed for modern apps and browsers which are expecting a DoT / DoT upstream. However, when your client is abroad, you should be using a VPN connection so even "plain" DNS is still encrypted in transit today.
The longer-running discussion lives on the original DoT request, for those who want the history:
Requested behaviour
Although there is an experimental implementation of DNS-over-TLS through the use of Stubby, official support coming to Pi-hole would greatly enhance the privacy aspects of the Pi-hole. DNS-over-TLS is in essence an encrypted tunnel through which the DNS-requests are send. Man-in-the-Middle (MitM) attacks on this traffic would result in captured encrypted data.
DNS-over-TLS (port 853) is not to be confused with DNS-over-HTTPS (port 443) and DNSCrypt (port 53). DNS-over-HTTPS…