Your Pi-hole, anywhere, exposed to no one

A Pi-hole at home answers the home network's DNS queries and refuses the ones for ad and tracker domains, so every device on the network gets the blocking without installing anything. Take the laptop out of the house and the blocking stays behind: the café's network hands out its own DNS server.

The obvious fix is the wrong one. Forwarding the router's port 53 to the Pi-hole makes it a DNS server that answers anyone, an open resolver, and open resolvers are what attackers use to amplify denial-of-service floods: a small query with a forged source address, a large answer sent to the victim. Pi-hole's own documentation warns that careless setups have created "an open resolver, which aids in DNS Amplification Attacks" (read November 1, 2026). So the home's DNS should answer the laptop and no one else.

We built that twice, the usual way and with nfltr, around the official Pi-hole image, and ran both through the same test.

The usual setup: a VPN home

Pi-hole's own documentation points to a VPN. PiVPN or wg-easy install WireGuard on the Pi-hole machine; the router forwards one UDP port (51820) to it; a dynamic DNS name follows the home's changing address; each device gets a key pair and a configuration whose DNS server is the Pi-hole's tunnel address.

It works, costs nothing, and the forwarded port belongs to WireGuard, not to a DNS server. It still needs a public IPv4 address (behind carrier-grade NAT nothing can be forwarded), a forwarded port, and the VPN connected on the laptop whenever it is away. Our version sent only the tunnel's subnet home; a full tunnel, the other common choice, sends every byte the laptop sends through the home's upload.

Before and after: a laptop using the home Pi-hole Before: the laptop's WireGuard connects in to the home router's forwarded UDP port, which passes it to WireGuard on the Pi-hole machine, and DNS queries travel inside the tunnel. After: the Pi-hole machine and the laptop each connect out to a relay; a stub resolver on the laptop sends the system's queries over TCP through an end-to-end encrypted session to Pi-hole. Before laptop WireGuard router UDP 51820 open Pi-hole box WireGuard, DNS job the laptop connects in; a key decides After laptop stub on :53 relay ciphertext only Pi-hole box no open port both connect out; TLS runs from the laptop to the box
Before, the home forwards a VPN port; after, both machines connect out and the home forwards nothing.

The same with nfltr

nfltr runs one small agent on each machine, and each agent connects out to a relay. nfltr's services carry TCP, and DNS has a TCP form that every DNS server answers, Pi-hole included (it is standard, RFC 7766). On the Pi-hole machine, nfltr up publishes Pi-hole's DNS port and its web interface, end-to-end encrypted:

agent: pihole
services:
  dns: { tcp: 53, e2ee: true }
  web: { tcp: 80, e2ee: true }

On the laptop, nfltr up maps them to local ports, pinned to the box's certificate:

connect:
  dns: { target: pihole/dns, listen: "127.0.0.1:5353", pin: "<the box's pin>" }
  web: { target: pihole/web, listen: "127.0.0.1:8080", pin: "<the box's pin>" }

The operating system asks its resolver over UDP, so one more piece sits on the laptop: a stub resolver (CoreDNS here) that answers the system on 127.0.0.1:53 and asks Pi-hole over TCP through the mapping, keeping that connection open between queries:

.:53 {
    bind 127.0.0.1
    forward . 127.0.0.1:5353 {
        force_tcp
    }
}

Point the laptop's DNS at 127.0.0.1 and its queries go to the home Pi-hole; its admin page is at http://127.0.0.1:8080/admin. The relay carries TLS records it cannot read, and a session opens only with your account key and the box's pin. Nothing at home accepts a connection from outside, and the home needs no public address.

We considered two other ways. nfltr's WireGuard mode carries DNS over UDP end to end, but its client side is Linux only and needs a kernel network device, like any VPN. DNS over HTTPS through a share link would reach a browser, but anyone holding the link could use it as a resolver, and Pi-hole would see every query from one client. We tested neither.

Phones are not covered. There is no nfltr app for iOS or Android, and Android's Private DNS speaks DNS over TLS to a hostname, on port 853 (Android's developer blog and RFC 7858, read November 1, 2026), which a share link (HTTPS on 443) cannot be. For a phone, the VPN is still the answer.

What we measured

Both setups ran as containers on one Mac: the Pi-hole machine behind a home router that adds 10 ms each way between home and the outside, a stand-in for the internet's DNS outside (Pi-hole's upstream, answering every name), a laptop outside, and in the nfltr setup a private relay outside. The laptop sent 100 cold queries (a name nobody had asked for, so Pi-hole asks its upstream) and 100 cached ones through each path.

path cold p50 / p95 cached p50 / p95
on the Pi-hole machine itself 25.3 / 29.3 ms 0.1 / 0.1 ms
WireGuard VPN 49.6 / 55.0 ms 23.3 / 27.9 ms
nfltr with the stub 50.8 / 57.9 ms 24.9 / 28.9 ms
nfltr, a new TCP connection per query 123.7 / 129.6 ms 74.4 / 81.9 ms

Through either setup a query costs the round trip home, 20 ms of it the delay we added; a second run agreed within 2 ms at the median. The last row is why the stub keeps its connection: without it, each query opens a session and an end-to-end TLS handshake through the relay. CoreDNS closes an idle connection after 10 seconds (its documented default, read November 1, 2026), so the first query after a pause pays that once.

Each check below passed, judged by a program's exit code:

  • Blocking works through the path. Two ad and tracker domains answered 0.0.0.0 from Pi-hole's blocklist, and a normal domain answered with its upstream address, in both setups.
  • Nothing answers DNS at home. In both setups, DNS over UDP and over TCP to the home's public address got no answer, and its ports 80 and 443 were closed. Before, the router had one forwarding rule (UDP 51820); after, none, and nothing on the laptop listened beyond loopback.
  • The admin page is yours only. The laptop reached Pi-hole's admin page over the tunnel before and through the mapping after. After, a session opened with a wrong account key was refused by the relay and one with the wrong pin failed closed.
  • A restart recovers. The box's side (WireGuard before, the box's agent after) was killed and started. The laptop's next answer came 18.4 s later through WireGuard, which kept using its old session until its handshake timed out, and 0.18 s later through nfltr.

The setup, counted

The VPN setup is 32 lines, most of them the two WireGuard configurations a tool such as PiVPN writes for you. The nfltr one is 34 lines with a private relay and 30 with the hosted one, including the stub's six. The VPN has five moving parts, nfltr six: the stub is the extra one.

before after
on the router a forward for UDP 51820 nothing
on the Pi-hole machine WireGuard, a dynamic DNS updater nfltr up, a certificate whose pin the laptop holds
on the laptop WireGuard, connected while away nfltr up, a stub resolver, the pin
keys a key pair per device the account key on both machines
outside a dynamic DNS name the relay

Two things change for Pi-hole: every laptop query reaches it from the box's own agent, so its statistics show one client for the laptop, and the laptop resolves nothing while the box is down, as with a VPN whose DNS points home.

What it costs

nfltr is free until further notice. To show what relaying this traffic costs whoever runs the relay, we priced it with our cost ledger at public cloud list prices. A laptop away from home all month at 150,000 queries, 145 bytes each through the relay, is 0.02 GiB. Relaying that end-to-end encrypted, plus two agents connected all month, comes to 0.017 USD a month at Google Cloud's list prices, almost all of it the connected agents, and 0.0005 EUR at Hetzner's.

The alternatives, at list prices read on October 31 and November 1, 2026:

alternative price
WireGuard home (PiVPN, wg-easy) free software; needs a public IPv4 address and a forwarded port
dynamic DNS name free (DuckDNS), or 30 USD a year (No-IP's paid tier)
behind carrier-grade NAT: a VM as the VPN's public end 4 USD a month (DigitalOcean, smallest)
a mesh VPN instead (Tailscale Personal) free, up to 6 users, non-commercial use
a hosted filtering resolver instead of Pi-hole (NextDNS) free up to 300,000 queries a month, then it stops blocking; Pro 159 INR a month (the page showed rupees when read from India)

Evidence: the home Pi-hole from a laptop away from home

Setup
Both setups as Linux containers on one Mac (Apple Silicon) with Docker, runs on October 31 and November 1, 2026; the numbers are from the last run, the one before agreed within 2 ms at every median. The official Pi-hole image (2026.10.1, pinned by digest) on a home network behind a router with 10 ms of added delay each way (tc netem), a CoreDNS stand-in for the internet's DNS as Pi-hole's upstream, a laptop network outside, a private relay outside in the nfltr setup. Kernel WireGuard configured as PiVPN and wg-easy do, with a split tunnel. No internet: Pi-hole's blocklist was a small file in the hosts format its default lists use, and the laptop resolved the home's name without a real dynamic DNS service.
Numbers
Cached query p50 0.1 ms on the Pi-hole machine, 23.3 ms over WireGuard, 24.9 ms through nfltr with the stub, 74.4 ms with a new TCP connection per query; cold p50 25.3, 49.6, 50.8 and 123.7 ms; ad and tracker domains 0.0.0.0 and a normal domain answered in both setups; no answer to DNS over UDP or TCP on the home's public address in both; router forwarding rules 1 and 0; a wrong account key refused, a wrong pin failed closed; next answer 18.4 s after WireGuard restarted and 0.18 s after the box's agent restarted; 32 and 34 configuration lines (30 with the hosted relay); 145 bytes per query through the relay; 0.017 USD a month at Google Cloud list prices, 0.0005 EUR at Hetzner's, for 150,000 queries and two agents connected.
Reproduce
(a) With the released binaries: run nfltr up with the first file above beside Pi-hole, nfltr up with the second and CoreDNS with the third on the laptop (the pin is in the box's nfltr up output), then set the laptop's DNS server to 127.0.0.1. (b) Our check, in the closed nfltr repository, runs both setups in containers and judges each claim by an exit code.
Limits
One host, so the only network distance is the delay we added and there is no loss; the relay sat next to the laptop. The stub and DNS settings were tested on Linux only. Phones, nfltr's WireGuard mode and DNS over HTTPS were not tested. Two runs are no distribution. The cost covers relaying only, at list prices, for an assumed month of queries; prices change, and each one names the date it was read.

Is it for you?

Fits if you run a Pi-hole (or any DNS server) at home and want its blocking on a laptop away from home without forwarding a port, behind carrier-grade NAT or not, with your queries unreadable to every machine in between, the relay included. The relay still sees which machines talk, when, and how many bytes; it can refuse or cut a session but cannot read or change one.

Not ready for phones: there is no nfltr app, and Android's encrypted DNS setting needs a DNS-over-TLS server on port 853, which nfltr does not provide. If every device you take out is a phone, a VPN home or a hosted filtering resolver is the better fit. The laptop also needs a stub resolver beside nfltr up, one more program to install.

Try next: install nfltr and sign in on both machines, then run the three files above. The nfltr up reference covers services: and connect:. Part 4 of this series does the same for Home Assistant, and part 7 sets nfltr beside a mesh VPN.

← All posts