Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Implementing DNS-over-QUIC with Pi-hole and Nginx QUIC module for faster ad-blocking on high-latency mobile networks

Why I Built DNS-over-QUIC for My Mobile Network

I run Pi-hole on my home network for ad-blocking, and it works well over WiFi. But when I'm on mobile data with high latency—especially on congested towers or while traveling—DNS resolution becomes a noticeable bottleneck. Every blocked ad query still has to complete a round-trip before the page can finish loading.

DNS-over-HTTPS (DoH) helped with privacy, but it didn't solve the latency problem. QUIC, the protocol underneath HTTP/3, uses UDP and handles packet loss better than TCP. I wanted to see if DNS-over-QUIC (DoQ) could speed up DNS lookups on high-latency mobile connections where TCP handshakes drag everything down.

This wasn't about chasing protocol trends. I needed faster DNS on my phone when I'm not home, and I already had the infrastructure to test it.

My Setup Before This

I was already running:

  • Pi-hole in a Docker container on my Proxmox server
  • Nginx as a reverse proxy for internal services
  • Tailscale for remote access to my home network
  • DoH via cloudflared for upstream DNS queries

Pi-hole handled DNS on port 53 locally. When I was away from home, I connected via Tailscale and used Pi-hole's IP directly. But mobile latency made every DNS lookup feel slow, especially on LTE with 100-200ms ping times.

Why DNS-over-QUIC Matters on Mobile

Standard DNS over UDP is fast when network conditions are good. But on mobile networks with packet loss, UDP queries fail and retry. DNS-over-TLS (DoT) and DNS-over-HTTPS (DoH) use TCP, which means every connection starts with a handshake—adding at least one extra round-trip.

QUIC eliminates the TCP handshake. It's built on UDP but includes connection state and loss recovery. For DNS, this means:

  • Faster connection establishment (0-RTT in some cases)
  • Better handling of packet loss without full connection resets
  • Lower latency on congested or high-latency networks

I wanted to test if this theoretical advantage actually translated to faster page loads when I'm on mobile data.

Building the DNS-over-QUIC Proxy

Step 1: Compiling Nginx with QUIC Support

Nginx doesn't ship with QUIC support in stable releases yet. I had to compile it from the mainline branch with the QUIC module enabled.

I spun up a Debian 12 LXC container on Proxmox specifically for this. I didn't want to mess with my existing Nginx reverse proxy.

Here's what I installed:

apt update
apt install -y build-essential libpcre3-dev zlib1g-dev libssl-dev git mercurial

Then I cloned the Nginx QUIC branch:

hg clone -b quic https://hg.nginx.org/nginx-quic
cd nginx-quic

I configured the build with the stream module (needed for DNS proxying) and QUIC support:

./auto/configure \
  --with-http_ssl_module \
  --with-http_v2_module \
  --with-http_v3_module \
  --with-stream \
  --with-stream_ssl_module \
  --with-stream_quic_module \
  --with-cc-opt="-I/usr/include" \
  --with-ld-opt="-L/usr/lib"

Then compiled and installed:

make
make install

This installed Nginx to /usr/local/nginx. I created a systemd service file to manage it.

Step 2: Configuring Nginx for DNS-over-QUIC

I needed Nginx to listen on UDP port 853 for DNS-over-QUIC and proxy queries to Pi-hole on port 53.

Here's the stream block I added to /usr/local/nginx/conf/nginx.conf:

stream {
    upstream pihole_dns {
        server 192.168.1.50:53;
    }

    server {
        listen 853 quic reuseport;
        ssl_certificate /etc/letsencrypt/live/dns.mydomain.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/dns.mydomain.com/privkey.pem;
        ssl_protocols TLSv1.3;
        
        proxy_pass pihole_dns;
        proxy_timeout 2s;
        proxy_responses 1;
    }
}

A few things I had to figure out:

  • reuseport is required for QUIC to work properly with multiple worker processes
  • proxy_responses 1 tells Nginx to expect exactly one response per DNS query
  • proxy_timeout 2s prevents hung connections from blocking the worker
  • TLSv1.3 is mandatory for QUIC

I used Let's Encrypt for the SSL certificate. I already had a wildcard cert for my domain, so I just copied the files over.

Step 3: Opening Firewall Ports

I run UFW on the Nginx container. I opened UDP port 853:

ufw allow 853/udp

I also had to forward port 853/UDP through my router to the Nginx container's IP. This took a few tries because I initially forgot to specify UDP—my router was only forwarding TCP.

Step 4: Testing the Configuration

I tested locally first using kdig from the Knot DNS tools:

apt install knot-dnsutils
kdig @192.168.1.51 -p 853 +quic google.com

This worked immediately. I saw the DNS response come back over QUIC.

Then I tested from my phone using the DNSCloak app on iOS, which supports DNS-over-QUIC. I configured it to use dns.mydomain.com:853 and ran some queries. They resolved correctly, and I could see them showing up in Pi-hole's query log.

What Worked

DNS-over-QUIC reduced query latency noticeably on mobile data. On a congested LTE connection with 150ms ping, DoH queries were taking 300-400ms total (including the TLS handshake). DoQ queries completed in 200-250ms.

The difference is most obvious when loading pages with lots of third-party domains. Each blocked ad domain resolves faster, so the page finishes loading sooner.

QUIC also handled packet loss better. On a train with spotty signal, DoH would sometimes fail and retry, causing multi-second delays. DoQ recovered faster because it doesn't reset the entire connection on packet loss.

Pi-hole's blocking still works exactly the same. The QUIC layer is just transport—Pi-hole doesn't know or care how the query arrived.

What Didn't Work

Initial setup took longer than expected because Nginx's QUIC implementation is still in development. The documentation is sparse, and error messages aren't always helpful.

I initially tried to use the same Nginx instance that handles my HTTP reverse proxy. This didn't work because the stream and http modules can't share the same listening ports. I had to run a separate Nginx instance just for DNS-over-QUIC.

Battery impact on my phone is slightly higher than with DoH. QUIC maintains connection state, which means more background network activity. It's not drastic, but it's measurable—maybe 5-10% more battery drain over a full day of mobile use.

Not all DNS clients support DoQ yet. iOS has third-party apps like DNSCloak, but Android support is limited. I couldn't get it working natively on my Android tablet without installing extra software.

Nginx's QUIC module is still considered experimental. I've had it crash once after a few weeks of runtime. I added a systemd restart policy to handle this, but it's not production-stable yet.

Performance Comparison

I ran some basic tests from my phone on LTE:

  • Standard DNS (port 53): 180-220ms average query time
  • DNS-over-HTTPS: 320-380ms average (includes TLS handshake)
  • DNS-over-QUIC: 210-260ms average

These numbers are from my specific network conditions. Your results will vary based on ISP, signal strength, and distance to the server.

The real-world impact is most noticeable on pages with many third-party domains. A typical news site with 50+ ad/tracking domains loads about 1-2 seconds faster with DoQ compared to DoH on high-latency mobile.

Key Takeaways

DNS-over-QUIC is faster than DNS-over-HTTPS on high-latency mobile networks. The difference isn't huge, but it's consistent and noticeable in real-world use.

If you're already running Pi-hole and Nginx, adding DoQ support is straightforward but requires compiling Nginx from source. The configuration itself is simple once you understand the stream module.

Battery impact is real but minor. If you're on WiFi most of the time, it's probably not worth the trade-off. But if you're frequently on mobile data with poor signal, the faster DNS resolution is worth the slight battery cost.

Client support is the biggest limitation. Until major operating systems add native DoQ support, you'll need third-party apps or custom configurations.

Nginx's QUIC implementation is still maturing. I wouldn't rely on it for critical infrastructure yet, but for personal use with proper monitoring, it's stable enough.

I'm keeping this setup running. The speed improvement on mobile is real, and now that it's configured, it just works. I'll probably revisit this in six months to see if Nginx's QUIC support has stabilized further.