Back to guides
Comparison

DoH vs DoT vs DNSCrypt vs DoQ

Plain DNS travels in the open, so anyone between you and your resolver can read or change it. There are four main ways to close that gap, plus DoH3, a faster form of one of them. They protect the same thing but make different trade-offs in how they travel, how easily they can be blocked, and how widely they are supported.

By Ozy-666, creator and operator of dnsdoh.art · Published · 17 min read
YOUR DEVICE RESOLVER DoT TCP + TLS · 853 DoH HTTPS · 443 DoH3 HTTP/3 · 443 DoQ QUIC · 853 DNSCrypt UDP / TCP · 443

Five lanes to the same place; the two QUIC-based lanes (DoH3, DoQ) move quickest. The difference is the road each takes and how hard it is to close.

They all carry the same payload, a DNS question and its answer, inside encryption. What separates them is the transport they ride on, the port they use, and the practical consequences of those two choices.

Why DNS gets encrypted at all

Traditional DNS runs over port 53 with no protection. The name you are looking up, and the address that comes back, travel as plain text. Anyone positioned on the path, the local network, the internet provider, anything in between, can read which sites you are reaching and can even alter the answer to send you somewhere else.

Encryption fixes two things at once: it hides the contents of the lookup from everyone on the path, and it keeps the reply from being tampered with on the way to your resolver. (Proving the records themselves are authentic end to end is a separate job, done by DNSSEC.) One point is worth keeping in mind throughout: encryption protects the lookup from the network, not from the resolver you chose. The resolver still sees your queries, so which resolver you trust matters as much as the protocol you use to reach it.

The four protocols

DNS over TLS (DoT)

RFC 7858 · 2016 · port 853

DoT wraps ordinary DNS in a TLS connection on its own dedicated port. It is the most straightforward of the four: the message format does not change, it is simply carried inside TLS. Because it has a port of its own, it is easy to reason about and easy to operate, which is why it became the standard for system-level encrypted DNS, including Android's Private DNS and systemd-resolved on Linux.

The same dedicated port is its weakness. Traffic on port 853 is unmistakably DNS, so a restrictive network can identify and block it without touching anything else.

DNS over HTTPS (DoH)

RFC 8484 · 2018 · port 443

DoH sends the lookup as an HTTPS request on port 443, the same port that carries nearly all web traffic. To anything watching the network it looks like an ordinary connection to a website, so it is the hardest of the four to single out and block. It is also the only one browsers implement directly, which is why Chrome, Edge, and Firefox can use encrypted DNS without any help from the operating system.

The cost is a little more overhead from the HTTP layer wrapped around each query, and the fact that a browser doing its own DoH can quietly disagree with the rest of the system, a common source of confusion and of leaks.

DNS over QUIC (DoQ)

RFC 9250 · 2022 · port 853

DoQ carries DNS over QUIC, the modern UDP-based transport that also underpins HTTP/3. It keeps the encryption and authentication of TLS while removing the head-of-line blocking that can stall TCP-based transports, and it sets up connections faster. That makes it noticeably better on lossy or high-latency links, such as mobile networks, where a dropped packet should not hold up everything behind it.

It is newer than DoH and DoT, so support is still growing, and like DoT it uses port 853, which a network can block. The next entry, DoH3, keeps QUIC's speed while moving back to port 443.

DNS over HTTP/3 (DoH3)

DoH (RFC 8484) carried over HTTP/3 · port 443

DoH3 is not a separate protocol so much as the best of the two before it combined: it is DNS over HTTPS, but the HTTPS rides on HTTP/3, which runs on QUIC. So it keeps DoH's defining advantage, port 443 that blends in with all other web traffic and resists blocking, while gaining QUIC's faster connection setup and its resistance to a single lost packet stalling the rest.

In practice it is the strongest all-round choice where it is available: hard to block like DoH, quick to recover like DoQ. Support depends on both the resolver and the client speaking HTTP/3, which is now common in current browsers and clients. This is the form this service runs natively.

DNSCrypt

community protocol (not an IETF standard) · v2 · usually port 443

DNSCrypt is the oldest of the group and the only one that is not an IETF standard. It predates DoH and DoT and uses public-key cryptography with signed certificates: the client verifies the resolver's certificate before trusting its answers. It is driven by a community client rather than being built into operating systems, so it needs that client installed to work.

Its standout feature is Anonymized DNSCrypt, which routes the query through a relay so the resolver answering it never sees your address, separating who is asking from what is asked. The standardized answer to this, Oblivious DoH, was specified but barely shipped, so in practice this remains DNSCrypt's own territory.

Side by side

  DoT DoH DoH3 DoQ DNSCrypt
TransportTCP + TLSHTTPS (TCP)HTTP/3 (QUIC)QUIC (UDP)UDP / TCP
Port853443443853443
StandardRFC 7858RFC 8484RFC 8484 + 9114RFC 9250Community
In the browserNoYesYesNoNo
Built into OSCommonGrowingGrowingLimitedClient
Blocking resistanceMediumHighHighMediumHigh
Latency on lossy linksFairFairBestBestFair
Source anonymity optionNoNoNoNoRelays

Ratings are practical generalisations, not absolutes; a specific resolver or network can shift any of them.

What we measured

Every comparison above is the sort of thing you can read anywhere. The rest of this page is different: we run all four encrypted transports on one resolver, so we can put the same query through each of them and count what comes back. These numbers are ours, taken on 29 and 30 July 2026.

How the measurements were taken

  • • A client in its own network namespace, connected by a veth pair with a 1500-byte MTU, so packets are segmented the way they are on a real path rather than the way they are on loopback.
  • • A fixed 20 ms delay in each direction, injected with netem on both ends of that veth. A round trip therefore costs 40 ms, which makes round trips visible in the timings instead of having to be inferred. The delay touches nothing but this harness.
  • • One client for every transport (q, Go), so differences are the protocols and not five different implementations. Each sample is a fresh process, which means a cold connection: no session resumption, no 0-RTT.
  • • The same question every time, example.com A, pre-warmed so it is served from cache and the resolver's own work is close to zero.
  • • Byte counts are IP-layer and exclude the Ethernet header. Medians of 9 runs for the packet tests, 300 runs for the loss test.

The harness is public: dns-transport-bench is the namespace setup, the measurement script and the raw output from this run, so the numbers below can be checked or repeated against any other resolver.

Two honest limits. This is one vantage point and one client stack: the round-trip counts, byte counts and loss behaviour are properties of the protocols and travel, but absolute milliseconds describe this path only. And we do not run a DNSCrypt endpoint, so DNSCrypt appears in the tables above but not in the measurements below.

1. How many round trips before the answer

On a path where one round trip costs exactly 40 ms, the time to a first answer on a cold connection reads directly as a round-trip count.

Time to first answer, cold connection lower is better · one round trip = 40 ms 1 RTT 2 RTT 3 RTT 4 RTT Do53 (plaintext): 40.8 ms, 1.0 round trip Do53 40.8 ms plaintext, no handshake DoQ (DNS over QUIC): 105.0 ms, 2.6 round trips DoQ 105.0 ms DoT (DNS over TLS): 143.6 ms, 3.6 round trips DoT 143.6 ms DoH over HTTP/2: 144.9 ms, 3.6 round trips DoH 144.9 ms DoH3 (DNS over HTTP/3): 147.9 ms, 3.7 round trips DoH3 147.9 ms
 Time to answerRound tripsPackets out / in
Do5340.8 ms1.01 / 1
DoQ105.0 ms2.64 / 7
DoT143.6 ms3.65 / 4
DoH144.9 ms3.68 / 4
DoH3147.9 ms3.74 / 7

DoQ reaches an answer a full round trip before DoT and DoH. That is QUIC doing what it was designed to do: the transport handshake and the TLS handshake are the same handshake, where TCP has to complete its own three-way handshake before TLS can start.

The result worth noticing is DoH3, which is also QUIC and yet lands with the TCP transports. The obvious explanation is that HTTP/3 spends the round trip QUIC saves, opening control streams and exchanging settings before the query can go out. We published that explanation, then captured the handshake, and it is not what happens.

The round trip goes to QUIC address validation. Our server runs with quic_retry enabled, so a new connection is answered first with a 119-byte Retry packet carrying a token, and the client must send its entire ClientHello a second time before the handshake proceeds. That is the whole difference: three round trips instead of DoQ's two. It is a setting on this resolver rather than a property of the protocol, so a DoH3 endpoint without address validation would not show the gap at all.

One DoH3 handshake, captured the extra round trip is address validation, not HTTP/3 clientserver 0 ms: the ML-KEM ClientHello does not fit one packet, so the client sends two Initials 0 ms 1 280 B 1 280 B 20 ms: the server answers with a Retry packet instead of a handshake 20 ms 119 B — Retry, with token 40 ms: the client repeats the identical ClientHello, now carrying the token 40 ms the same 2 × 1 280 B again 61 ms: the server sends its entire flight in one burst, with no anti-amplification stall 61 ms 5 189 B, one burst, no stall 104 ms: the client finishes the handshake and sends the DNS query 104 ms Finished + query answer at 125 ms · 3 round trips, one of them spent on Retry

2. Bytes on the wire for one lookup

The same question and the same answer, counted from the first packet of the connection to the last. This is the true cost of a cold lookup, handshake included.

Bytes on the wire, one cold lookup IP layer, handshake included · lower is better client → server server → client Do53: 71 B out, 146 B in, 217 B total Do53 217 B the whole exchange fits in two packets DoT: 1770 B out, 5063 B in, 6833 B total DoT 6 833 B DoH over HTTP/2: 2231 B out, 5007 B in, 7238 B total DoH 7 238 B DoQ: 4019 B out, 5723 B in, 9742 B total DoQ 9 742 B DoH3: 5232 B out, 5623 B in, 10855 B total DoH3 10 855 B Do53 is 217 bytes. The cheapest encrypted transport costs 31 times that, and all of the difference is handshake.
 Client → serverServer → clientTotalvs Do53
Do5371 B146 B217 B
DoT1 770 B5 063 B6 833 B31×
DoH2 231 B5 007 B7 238 B33×
DoQ4 019 B5 723 B9 742 B45×
DoH35 232 B5 623 B10 855 B50×

Around 5 KB of every encrypted lookup is the same thing in all four cases: the server's certificate chain. That is a fixed cost paid once per connection and amortised over every query that follows, which is the entire argument for keeping DNS connections open rather than opening one per lookup.

The QUIC transports send noticeably more from the client. QUIC pads its initial packets to at least 1200 bytes as an anti-amplification measure, so a small ClientHello still costs a full packet. Hold on to that detail, because it produces the strangest result on this page.

3. What happens on a lossy link

The same 40 ms path, with 2 % of packets dropped in each direction: roughly what a weak mobile signal or a congested cafe network looks like. Three hundred cold lookups per transport, because a tail measured on a handful of samples is a single unlucky draw rather than a number.

Latency under 2% packet loss, each direction median → 95th → 99th percentile of 300 cold lookups median 95th 99th 0 300 ms 600 ms 900 ms 1200 ms Do53: median 40.8 ms, 95th 41.0 ms, 99th 41.3 ms, but 12 of 300 lookups returned nothing at all Do53 flat 41 ms, but 12 of 300 lookups simply never came back DoQ: median 105.2 ms, 95th 236.5 ms, 99th 310.3 ms DoQ 310 ms DoH3: median 145.7 ms, 95th 286.7 ms, 99th 329.4 ms, with 15 of 300 lookups returning no answer DoH3 329 ms 15 of 300 returned no answer DoT: median 144.2 ms, 95th 477.9 ms, 99th 1225.8 ms, worst 3379 ms DoT 1226 ms DoH over HTTP/2: median 144.2 ms, 95th 522.2 ms, 99th 1222.1 ms, worst 1572 ms DoH 1222 ms At the median, loss barely registers. The difference is entirely in the tail, and the tail is where users notice.
 Median95th99thWorst seenMeanNo answer
Do5340.8 ms41.0 ms41.3 ms42.9 ms40.8 ms12 of 300
DoQ105.2 ms236.5 ms310.3 ms378.9 ms124.7 ms0 of 300
DoH3145.7 ms286.7 ms329.4 ms494.8 ms163.2 ms15 of 300
DoT144.2 ms477.9 ms1 225.8 ms3 379.1 ms217.1 ms0 of 300
DoH144.2 ms522.2 ms1 222.1 ms1 571.5 ms210.3 ms0 of 300

Half of all lookups are untouched by 2 % loss on every transport: each median sits within a millisecond of what the same transport does on a clean path. Nothing separates them until the tail, and then the split is not gradual. The two TCP transports reach 1.2 seconds at the 99th percentile. The two QUIC transports stop at 310 and 329 ms.

The number that explains it is one second. When TCP loses a packet during a handshake there are no later packets to infer the loss from, so it cannot fast-retransmit; it waits out a retransmission timeout, and RFC 6298 puts the floor on that timeout at one second. The measurement lands on it precisely: DoT's 99th percentile is 1 082 ms above its own median and DoH's is 1 078 ms above its own, two independent transports agreeing to within four milliseconds. That is not a latency distribution, it is a constant.

Lose a second packet and the timer doubles, which is what DoT's worst sample is: 3 379 ms, a median plus one second plus two. QUIC's loss recovery is not bound to that floor, and neither QUIC transport went past 495 ms in 300 lookups.

Plain DNS is the instructive case. It is fast in every sample that arrives, because it has no recovery mechanism at all: a lost packet is not retransmitted, it is a lookup that never returns. Twelve of 300 vanished, close to the roughly 4 % you would predict for a single packet each way at 2 % loss. The encrypted transports all recover; they differ only in how gracefully.

The transport that fails worst is ours, and it turned out to be a bug in nginx

Look again at the last column. DoH3 returned no answer on 15 of its 300 lookups, a worse failure rate than plain unprotected DNS. Its tidy 329 ms tail is partly an illusion created by those missing samples: the lookups that would have been slowest are not in the distribution, because they were abandoned instead. Chasing that number down took the rest of the day and ended somewhere we did not expect.

Every failure is a handshake that never completed. Four controls narrowed it:

  • • Forcing the classical key exchange, which shrinks the handshake and changes nothing else, gave 100 successes out of 100.
  • curl, an entirely different QUIC implementation, fails at the same rate as our Go client. Not a client bug.
  • DoQ uses the same client, the same post-quantum key exchange and the same certificate, and failed zero times in 300. Not QUIC, and not the cryptography.
  • • Both wrong guesses we published along the way, the anti-amplification limit and QUIC address validation, were tested and eliminated. Turning address validation off saved a round trip and left the failures exactly where they were.

What was left was the server, so we captured a failing handshake and decrypted it. QUIC's Initial packets can be read without any key material, because their keys derive from the connection ID, and that is precisely the part that matters here.

The post-quantum ServerHello does not fit in one packet. Its ML-KEM key share is 1 088 bytes, which pushes the ServerHello to about 1 178 and splits it across two Initial packets. Lose one of the two and the client holds half a ServerHello: it cannot derive handshake keys, so every packet that follows is undecryptable to it. That much is just packet loss, and recovering from it is routine.

nginx does not recover from it. Its own debug log shows the server detecting the loss and deciding to retransmit, and then never putting the data back on the wire:

frame tx init:1  CRYPTO len:1124 off:0      ← ServerHello, part 1
frame tx init:2  CRYPTO len:54   off:1124   ← part 2
frame rx init:4  ACK n:1 delay:0 2 0        ← client acks 2 and 0, never 1
detect_lost pnum:1 ... level:0          ← nginx sees the loss
resend packet pnum:1                    ← and decides to resend it
frame tx init:3  ACK only
frame tx init:4  ACK only
frame tx init:5..10  ACK only, every one
frame tx init:11 CONNECTION_CLOSE

The CRYPTO frame is sent once and never again. Eight further Initial packets carry nothing but acknowledgements, while every probe timer fires against the Handshake encryption level even though the missing data sits in the Initial one. The client meanwhile does everything right: it acknowledges what arrived, repeats its own ClientHello, and backs off exponentially until it gives up. Two correct participants, one of them withholding the single thing the other needs.

It reproduces on a completely stock nginx, built from unmodified source with a twenty-line configuration, at the same rate. It needs one more ingredient we nearly missed: a certificate chain long enough to span several Handshake packets. With a single small self-signed certificate it did not appear once in 150 attempts, which would have made for an embarrassing bug report.

We have reported it upstream as nginx/nginx#1616, with the packet captures, the debug log and a scripted reproduction published at nginx-quic-initial-repro. Until it is fixed, read DoH3's 329 ms as "when it answers", and note that the priority-1 record we publish for discovery offers HTTP/2 alongside HTTP/3, so a client that cannot complete the QUIC handshake falls back to TCP rather than failing. If you run nginx with HTTP/3 and post-quantum key exchange, this affects you too.

4. What the post-quantum handshake costs

All four transports here negotiate X25519MLKEM768, a key exchange designed to stay secure against a future quantum computer. It is bigger than the classical one. Running the same client twice, once with the post-quantum group and once with classical X25519 forced, prices it exactly.

Bytes for one cold lookup: classical vs post-quantum same client, same transport, key exchange group forced classical X25519 + ML-KEM 768 DoT: 4749 B classical, 7065 B post-quantum, +2316 B DoT 4 749 B 7 065 B +2 316 B DoH over HTTP/2: 4469 B classical, 7237 B post-quantum, +2768 B DoH 4 469 B 7 237 B +2 768 B DoQ: 6985 B classical, 9853 B post-quantum, +2868 B DoQ 6 985 B 9 853 B +2 868 B DoH3: 8486 B classical, 12416 B post-quantum, +3930 B DoH3 8 486 B 12 416 B +3 930 B
 ClassicalPost-quantumCostTime change
DoT4 749 B7 065 B+2 316 Bnone
DoH4 469 B7 237 B+2 768 Bnone
DoQ6 985 B9 853 B+2 868 B40 ms faster
DoH38 486 B12 416 B+3 930 Bnone

Between 2.3 and 3.9 KB, once per connection, and on three of the four transports it costs no measurable time at all: the extra bytes ride in packets that were being sent anyway. Then there is DoQ, where the post-quantum handshake came out a full round trip faster than the classical one. That is not a measurement error.

Why the bigger handshake is the faster one

A QUIC server may not send more than three times the bytes it has received from an address it has not yet validated. It is the rule that stops QUIC servers being used as reflection amplifiers. With the classical key exchange our server has 4 131 bytes to send, and that rule decides whether it may.

The same DoQ handshake, captured twice Classical X25519 first flight 1 308 B → budget 3 × 1 308 = 3 924 B clientserver 0 ms: the client sends one 1308-byte Initial packet 0 ms 1 308 B 21 ms: the server sends 3 x 1308 B and stops, exactly at the anti-amplification limit 21 ms 1 308 B 1 308 B 1 308 B budget spent at 3 924 B the server is not allowed to continue 61 ms: the client's second flight raises the budget 61 ms 1 308 B 82 ms: the rest of the handshake, then the answer 82 ms 207 B answer at 152 ms · 4 round trips With ML-KEM 768 first flight 2 616 B → budget 3 × 2 616 = 7 848 B clientserver 0 ms: the larger ClientHello does not fit in one packet, so the client sends two Initials 0 ms 1 308 B 1 308 B 21 ms: the server sends its whole 5278-byte flight at once, well inside the larger budget 21 ms 1 308 B 1 308 B 1 308 B 1 354 B 5 278 B sent in one flight, no stall 63 ms: the client finishes the handshake and sends the DNS query 63 ms 1 403 B answer at 105 ms · 3 round trips

With the classical key exchange the ClientHello fits in one packet, so the client's first flight is 1 308 bytes and the server may answer with at most 3 924. It has 4 131 bytes to send. It sends exactly 3 × 1 308 bytes, stops 207 bytes short of finishing, and waits for the client to speak again before it is allowed to send them. That wait is the extra round trip, and the capture shows those 207 bytes arriving 21 ms after the client's next packet.

The post-quantum ClientHello is too large for one packet. The client sends two, so the budget doubles to 7 848 bytes. The server's flight is larger too, 5 278 bytes rather than 4 131, because the key share it carries is bigger - and it still fits with room to spare. The handshake completes a round trip sooner. The post-quantum key exchange pays for itself by lifting the client over the limit.

The lever underneath is handshake size, not cryptography. A server whose flight fits inside 3 924 bytes never stalls in the first place, which is a concrete argument for short certificate chains, ECDSA rather than RSA, and no unnecessary intermediates. It also means the result is ours rather than universal: on a resolver with a smaller certificate chain, classical DoQ would not stall, and this asymmetry would not appear.

What the numbers change. Not much about privacy: all four encrypt the lookup, and the choice between them was never a privacy choice. What they do settle is the performance question that the tables above can only gesture at. DoQ is genuinely the quickest to a first answer and the steadiest when packets go missing. DoH3 inherits QUIC's loss behaviour but hands QUIC's handshake advantage back to address validation, which is our setting to change rather than a fact about the protocol. DoT and DoH are indistinguishable at the median and differ only in their tails, where DoH is the more exposed of the two.

And every one of these differences is a cold-connection difference. A resolver connection that stays open turns all of this into a single first lookup, after which every transport costs one round trip and a couple of hundred bytes. Which is the strongest practical argument on this page: whichever you pick, pick the one your device will keep open.

These measurements are published under CC BY 4.0. Reuse the numbers, the tables and the charts freely, including commercially, as long as you credit dnsdoh.art and link back to this page.

What actually separates them

Privacy from the network is a tie. They all hide the lookup contents from everyone between you and the resolver. If that is your only goal, any of them does the job.

Privacy from the resolver is where they part. By default the resolver you connect to still sees every query, whichever protocol you used. The practical way to break that link is a relay: the client sends each query through a relay so the resolver answering it never learns your address. Anonymized DNSCrypt does exactly this, set up in dnscrypt-proxy. Oblivious DoH was the standardized attempt at the same idea but never saw real deployment, so for now relays are effectively DNSCrypt's domain.

Resistance to blocking follows the port. DoH, DoH3, and DNSCrypt sit on 443 alongside normal web traffic and are hard to isolate. DoT and DoQ sit on 853, which a network can block outright when it wants to force DNS back into the open.

Performance and reach favour different winners. DoQ and DoH3 handle packet loss best and connect fastest; DoT and DoH are well supported and perfectly fast on stable links; DoH and DoH3 are the ones that work inside a browser with no system changes; DNSCrypt needs its own client but brings relay anonymity the others lack natively.

Which should you use

If you only want the short answer, here is how the five sort out on the questions people actually ask. Each verdict assumes the resolver and the client both support the protocol.

Fastest

DoQ & DoH3

Both ride QUIC, so they finish their handshake in fewer round trips and a single lost packet never holds up the ones behind it. The lead is widest on mobile and other lossy links; on a stable wired connection the rest are close behind.

Most stable & compatible

DoT & DoH

The mature pair. DoT is the native standard on Android and Linux; DoH is built into every major browser and into Windows 11. If you want something that simply works on the widest range of devices today, start with these two. Recent systems can also find them on their own: Windows 11 and Apple platforms use automatic discovery to upgrade a plain resolver to its encrypted equivalent.

Hardest to block

DoH & DoH3

Both sit on port 443 next to ordinary web traffic, so a network cannot single them out without breaking the rest of the web. On a restricted or filtered connection this is usually the deciding factor. DNSCrypt on 443 is a close third.

Easiest to turn on

DoH & DoT

DoH is a single toggle in Chrome, Edge, Firefox, and Windows 11. DoT is one hostname field in Android's Private DNS. Neither needs an extra app or a configuration file, unlike DNSCrypt, which needs its own client.

Best for source privacy

DNSCrypt

Anonymized DNSCrypt routes your query through a relay (configured in dnscrypt-proxy), so the resolver that answers never sees who asked. Oblivious DoH aimed to bring the same split to DoH but never really shipped, which leaves DNSCrypt's relays the one option you can actually use today.

Best all-round

DoH3

Where it is supported it gives you everything that matters at once: the port-443 blending that resists blocking, QUIC's speed and loss recovery, and browser support with no extra software. If your resolver and client both speak it, it is the strongest default. It is the form this service runs natively.

What matters more than the protocol

Once the lookup is encrypted, the larger questions are who answers it and whether your setup actually uses the encrypted path. A protocol you trust pointed at a resolver you do not is a poor trade, and an encrypted resolver that your device quietly bypasses protects nothing.

This service answers over DNS over HTTPS, DNS over TLS, and HTTP/3; the protocols page lists what is offered and the setup guide has the per-platform steps. After you configure any of them, confirm it took effect: how the DNS leak test works explains the check, and you can run the leak test to see which resolver actually answered.

Pick one and confirm it works

Configure an encrypted protocol, then verify the lookups really travel it.

Ozy-666 Author · Creator of dnsdoh.art

Ozy-666 builds and operates dnsdoh.art, an encrypted DNS resolver serving DoH, DoH3, DoT and DoQ. He maintains its Edge-optimised AdGuardHome and dnscrypt-proxy forks and the custom resolver, TLS and firewall stack these guides document. They are written from running that infrastructure day to day.