Every device on a network needs an address. The old and still dominant scheme, IPv4, writes them as four numbers from 0 to 255: 192.168.1.50. That gives about 4.3 billion possible addresses. There are more phones than that alone, so the world ran out.
The workaround was to carve out three ranges and declare them private: addresses that routers on the public internet agree to never forward. Anyone can use them, inside their own network, as many times as they like.
10.0.0.0/8 172.16.0.0/12 192.168.0.0/16192.168. is fixed and anything can follow.10.anything.anything.anything is in the block. A smaller number after the slash means a bigger block: 8 fixed bits leaves 24 free, which is about 16.7 million addresses.Your laptop is probably 192.168.1.something. So is a laptop in most houses on your street. These addresses collide constantly and nobody cares, because they are only meaningful inside one local network. Your ISP gives your router one genuinely globally unique address - the public IP - and everything behind it shares that one.
A private IP is a nickname, not a name. 192.168.1.50 means "the Pi" in your house and something else entirely in mine, so it cannot be dialled from outside.
If ten devices share one public IP, replies coming back need to be sorted out somehow. The router does this with NAT, Network Address Translation, and the mechanism is a bookkeeping table.
When your laptop sends a packet out, the router rewrites the source from 192.168.1.20 to its own public IP, picks an unused port number, writes a row in its table, and sends it on. When the reply arrives addressed to that port, it looks up the row, rewrites the destination back, and delivers it.
192.168.1.20:44310 ↔ 203.0.113.7:54021 → 93.184.216.34:443Now notice when that row gets created: only because your laptop reached out first. A packet arriving unprompted from the internet has no matching row. The router is not being strict or secure - it genuinely has no way to know which of your ten devices the packet was meant for. So it drops it.
This asymmetry is the whole problem, and it is worth sitting with. It is not a setting someone turned on. It falls out of the addressing scheme. It is also, by accident, most of what protects home networks: an attacker scanning the internet cannot reach your printer for the same reason you cannot reach your Pi.
NAT is not a firewall someone configured - it is a lookup table that only has entries for conversations you started. Outbound creates permission; inbound has none to find.
Before any of that packet-shuffling happens, a device needs to turn a name like ads.example.com into an address. That translation is DNS, the Domain Name System, and it runs as a chain of increasingly specific questions.
Your device's stub resolver - a small piece of the operating system - does not know anything. It asks a recursive resolver, the one address handed to it by DHCP when it joined the network. The recursive resolver is the one that does real work: it asks a root server who handles .com, asks that TLD server who handles example.com, and asks that authoritative server for the actual record. Then it caches the answer and hands it back.
Pi-hole slots into exactly one place in that chain. Your router hands out Pi-hole's address as the resolver, so every query arrives there first. Pi-hole is a forwarding resolver with a blocklist: it does not walk the root/TLD/authoritative chain itself by default, and it is not authoritative for anything. It checks the name against its blocklists. On a miss, it forwards the question upstream (Cloudflare, Quad9, whatever you configured) and passes the real answer back. On a hit, it never forwards at all - it answers immediately with 0.0.0.0, or an NXDOMAIN "no such name", depending on how you set the blocking mode.
Two consequences follow directly from this shape, and they explain most of what people find confusing about Pi-hole. First, it can only block things that are looked up by name - an app that hardcodes an IP address never asks, so it is never blocked. Second, it blocks the whole domain or none of it, because a name is all it ever sees. It cannot remove an ad from inside a page that loads from the same domain as the content, which is why YouTube ads survive it.
Pi-hole never touches your actual traffic. It only answers the question "what address is this name?" - and for a blocked name it answers with a dead one, so the connection is never attempted.
Put §01 and §03 together. Pi-hole blocks ads on your phone because your router tells the phone "your DNS server is 192.168.1.50". Step outside and join a cellular network, and your carrier's DHCP tells the phone something else. You cannot correct it by typing in 192.168.1.50, because out there that string addresses nothing at all - and even if it did, §02 says the packet would die at your router anyway.
So the requirement is sharper than "I want a VPN". It is: give the Pi an address that means the same thing everywhere. Once the phone can reach the Pi from any network, Pi-hole works unchanged - it does not need to know the phone left the house.
That is what a VPN actually provides. Not encryption first, not privacy first, though it gives you both. It builds a second, virtual network layered on top of the real one, where every member gets a stable address that travels with the device. The two products below are two different ways to build that overlay.
The problem was never Pi-hole. The problem is that your Pi has no address reachable from outside, so a VPN is really an addressing fix that happens to also encrypt.
Both answers use the same tunnel technology underneath, so learn it once. WireGuard is a VPN protocol built into the Linux kernel, deliberately tiny - a few thousand lines against OpenVPN's hundreds of thousands.
It has no accounts and no login. Each peer generates a Curve25519 keypair: a private key it never shares, and a public key it hands out. A peer's public key is its identity. If a packet decrypts correctly with a known peer's key, it is from that peer; if not, WireGuard drops it silently and does not reply at all. From the outside, a WireGuard port is indistinguishable from a closed one, which is why it survives constant internet scanning without fuss.
The setting worth understanding properly is AllowedIPs, because it does two jobs at once and beginners usually only notice one.
[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 10.6.0.0/24, 192.168.1.0/24
Endpoint = home.example.org:51820This is where full tunnel versus split tunnel comes from. AllowedIPs = 0.0.0.0/0 is a prefix length of zero: no bits fixed, so every possible address matches. Everything goes home, which gets you Pi-hole DNS plus all your browsing routed through the house - bounded by your home upload speed. A narrow list like 10.6.0.0/24, 192.168.1.0/24 sends only tunnel and home-LAN traffic, which is faster, but then DNS only reaches Pi-hole if the Pi's address is inside that list.
WireGuard has no accounts - the public key is the identity, and unrecognised packets get silence rather than a rejection. AllowedIPs is simultaneously "what I send here" and "what I accept from here".
The direct answer to §02 is to write a NAT row yourself, in advance, that never expires. That is port forwarding: you tell the router "anything arriving on UDP port 51820 goes to 192.168.1.50, no matter who sent it." PiVPN is the installer that sets up the WireGuard side of this on a Raspberry Pi in a few prompts; the router half is yours to do.
It works, and it is fully self-hosted - no third party is involved in any part of it. The costs are real though, and they are worth knowing before you choose:
Port forwarding is a NAT row you write by hand so it exists before anyone knocks. It needs router access, a stable public address, and no CGNAT above you - three assumptions that quietly fail more often than people expect.
The second answer takes §02 seriously rather than fighting it. If outbound packets always create permission, then let both sides send outbound at the same time. Each one's packet punches a row in its own NAT, and the other's arriving packet then matches that row and is accepted as if it were the reply. Neither router was configured. Both were tricked, symmetrically, by ordinary use.
The catch is that each side must learn where to aim - its peer's current public IP and port - and neither can be reached to be asked. That is what a coordination server is for, and it is the only piece a system like Tailscale adds. Every device holds an outbound connection to it, which NAT permits freely. Over that channel, peers exchange public keys and their observed public address and port (learned via STUN, a protocol whose sole job is to tell a device how the outside world currently sees it). Then both start sending. The coordination server never carries your data - once the punch lands, traffic is direct, peer to peer, encrypted by plain WireGuard from §05.
Be honest about when this fails. Hole punching depends on the NAT being endpoint-independent: it reuses the same external port for a given internal socket regardless of who the packet is going to, so the port STUN reported is the port your peer can hit. Many home routers behave this way. Symmetric NAT does not - it picks a fresh external port per destination, so the address your peer was told is already stale by the time it aims. Then the punch cannot land, and traffic falls back to a relay (Tailscale calls these DERP servers): slower, adds a hop, but still end-to-end encrypted, so the relay forwards ciphertext it cannot read.
The overlay addresses come from 100.64.0.0/10, a range reserved for exactly this kind of shared-address-space use. Devices get a stable address there and keep it wherever they roam - which is precisely the property §04 asked for. Point your tailnet's DNS setting at the Pi's 100.x address and Pi-hole filters your phone on cellular, with no port forwarded anywhere.
The trade you are making: the coordination server is run by a company. It cannot read your traffic - it only ever sees public keys and endpoints - but it does decide who may join your network, and if it is unreachable, new connections cannot be established (existing tunnels keep running). Headscale is an open-source reimplementation of that server you can host yourself, and the official clients speak to it.
Nobody opens a port. Both sides dial out at the same instant and each one's NAT accepts the other's packet as a reply. It needs a matchmaker to know where to aim, and it degrades to a relay rather than failing when the NAT is symmetric.
Both options run WireGuard and both end with encrypted peer-to-peer traffic. The technical difference sits entirely in how the first packet gets through, and that difference resolves into a dependency you have to accept either way.
PiVPN depends on your router, your public IP, and your ISP not using CGNAT. Nothing external, nothing that can be revoked by a company. If any of those three assumptions breaks, you are stuck.
Tailscale depends on a coordination server you do not run. Nothing about your router matters, CGNAT is irrelevant, and there is no port to forward or hostname to keep fresh. If the company changes terms or the service is down, new connections stop forming.
For the concrete case that started this - Pi-hole at home, ad blocking on a phone that leaves the house - Tailscale is the more robust path, because the failure modes it removes (CGNAT, rotating IP, locked router) are ones you cannot fix, and the one it introduces (a third-party matchmaker) is one you can escape later by moving to Headscale without changing any client.
One configuration detail bites people on both paths, and it follows from §03: Pi-hole decides whether to answer based on which interface a query arrived on. A query from the tunnel arrives on wg0 or tailscale0, not the LAN interface. Left on the default "allow only local requests", Pi-hole silently drops it - the tunnel shows connected and nothing resolves.
Neither option is more secure than the other in transit - both are WireGuard. You are choosing whether your single point of failure is your ISP's network design or someone else's control plane.
IPv4 ran out of addresses, so homes use private ranges like 192.168.0.0/16 that are nicknames rather than names, and a router shares one public address among them using a NAT table whose rows are only ever created by outbound traffic - which means unsolicited inbound packets have no row to match and get dropped, and that single asymmetry is why your Pi is unreachable from a cafe. Pi-hole is unaffected by any of this at home because it works one layer up, as a forwarding resolver that answers 0.0.0.0 for names on a blocklist and forwards everything else, but the moment your phone leaves the house it is handed a different resolver and your Pi's private address addresses nothing. So what you actually need is not encryption but an address for the Pi that means the same thing everywhere, which is what a VPN overlay provides: WireGuard carries it, identifying peers by public key alone and using AllowedIPs as both routing rule and access filter. Getting that first packet through has two answers - port forwarding writes a permanent NAT row by hand and depends on you controlling the router and not sitting behind CGNAT, while hole punching has both peers dial out simultaneously through a coordination server so each NAT opens itself, working without any router configuration and relaying through DERP when a symmetric NAT defeats the punch. The choice between them is not about security, since both are the same tunnel, but about which single point of failure you would rather own.