Intuition Blueprint

Why can't I just connect to my Pi from a cafe?NAT, DNS, and the two ways a VPN answers

created: 2026-07-23 last-updated: 2026-07-23 networking, nat, dns, vpn
01 - the address

Your Pi's address is a nickname that only works inside your house

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.

the three private ranges, in CIDR notation
10.0.0.0/8    172.16.0.0/12    192.168.0.0/16
reading it
  • 192.168.0.0 - the first address in the block, written as four 8-bit numbers. Under the hood it is one 32-bit number; the dots are just for humans.
  • /16 - the prefix length. It says the first 16 bits of the address are fixed and identify the network; the remaining 16 bits are free to number devices. So 192.168. is fixed and anything can follow.
  • /8 - only the first 8 bits are fixed, so 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.
  • the whole notation - "a starting address, plus how many leading bits are locked". It is how you describe a range of addresses in one token instead of writing out the first and last.

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.

Remember

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.

02 - the asymmetry

Your router remembers who asked, and drops everything else

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.

one row in the NAT table
192.168.1.20:44310  ↔  203.0.113.7:54021  →  93.184.216.34:443
reading it
  • 192.168.1.20:44310 - the private address of your laptop, and the port it chose. A port is a 16-bit number that says which conversation on that device the packet belongs to, so one device can hold many at once.
  • 203.0.113.7:54021 - your router's public address, and a port the router picked to stand in for your laptop's port. This is the pair the outside world sees.
  • 93.184.216.34:443 - who the packet is going to. Port 443 is the conventional port for HTTPS.
  • the whole row - "traffic coming back to my port 54021 from that server belongs to the laptop's port 44310". Without the row, a returning packet is unattributable.

Now 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.

outbound: works your laptop 192.168.1.20 router (NAT) writes a row a server out there 93.184.216.34 the reply comes back and matches the row, so it gets delivered unsolicited inbound: dropped your Pi never hears it router (NAT) no matching row you, at the cafe 203.0.113.7 ... then what?
Fig. 1 - the same router, the same wire, two directions. Blue traffic creates its own permission. Red traffic has none.

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.

Remember

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.

03 - the name lookup

Pi-hole blocks ads by lying about addresses, not by filtering traffic

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.

your phone stub resolver who is? Pi-hole forwarding resolver + blocklist on list answers 0.0.0.0 itself nothing leaves the house not on list upstream resolver 1.1.1.1 / 9.9.9.9 root -> .com -> authoritative the real chain, walked once, then cached
Fig. 2 - Pi-hole sits at the first hop. Red = answered locally with a dead address. Blue/orange = forwarded to a real resolver.

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.

Remember

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.

04 - the gap

Ad blocking stops at the front door, and now you know exactly why

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.

Remember

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.

05 - the tunnel

WireGuard identifies peers by key, and one setting does two jobs

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.

a peer block in a WireGuard config
[Peer]
PublicKey = xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
AllowedIPs = 10.6.0.0/24, 192.168.1.0/24
Endpoint = home.example.org:51820
reading it
  • [Peer] - a block describing one other machine in the tunnel. A config has one of these per peer you talk to.
  • PublicKey - that peer's Curve25519 public key, base64-encoded. This is the peer's whole identity; there is no username anywhere.
  • AllowedIPs - a list of CIDR blocks (see §01) that carries two meanings simultaneously. Outbound, it is a routing rule: traffic destined for these ranges goes into this tunnel. Inbound, it is an access filter: a decrypted packet from this peer is accepted only if its source address falls in these ranges.
  • Endpoint - where to send the encrypted packets right now: a hostname or IP, and a UDP port. Optional, because a peer that has no fixed address can stay silent and be discovered when it speaks first.
  • the whole block - "this key is that machine, these address ranges live behind it, and here is where to reach it."
AllowedIPs 10.6.0.0/24 going out a routing rule: what enters the tunnel coming in an access filter: which sources count
Fig. 3 - one line, read in two directions. Widening it to route more traffic also widens what you will accept.

This 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.

Remember

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".

06 - answer one

Port forwarding: one hole, opened by hand, permanently

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:

Remember

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.

07 - answer two

Hole punching: two doors, opened from the inside, at the same moment

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.

coordination server keys + observed ip:port both outbound, so both allowed your phone behind carrier NAT 100.71.4.9 your Pi behind home NAT 100.82.17.3 direct WireGuard tunnel the server is out of the path from here on
Fig. 4 - green addresses are overlay addresses, stable wherever the device goes. Dashed blue is signalling only; the thick green line carries the data.

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.

Remember

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.

08 - choosing

The real choice is which dependency you would rather carry

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.

Remember

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.

In one breath

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.

sources WireGuard paper How NAT traversal works Pi-hole docs RFC 1918 (private ranges)