A complete, beginner-friendly tutorial for building a "home-exit" WireGuard VPN from nothing. If you follow every step in order, you will end up with a VPN where your phone and laptop — anywhere in the world — browse the internet as if they were sitting at home, with home ad-blocking and access to your home network.
No prior WireGuard experience is assumed. Commands are copy-paste ready; placeholders in <ANGLE_BRACKETS> are the only things you change.
A normal commercial VPN sends your traffic out through the VPN company's servers. This one sends your traffic out through your own house. That gives you things a commercial VPN can't:
The problem: your home internet connection usually has no fixed public address, and your router blocks unsolicited inbound connections. So your devices can't just "dial home".
The solution: rent a cheap always-on cloud server with a public IP and use it as a meeting point (a "hub"). Everything connects out to the hub — including a small device at home (a Raspberry Pi) that keeps a permanent tunnel open. The hub then relays your phone/laptop traffic down that home tunnel and out to the internet.
YOUR DEVICES (phone, laptop) ── anywhere
│ connect out to the hub
▼
┌───────────────────────────────┐
│ HUB — cloud VM, public IP │ the meeting point (relay only)
└───────────────┬───────────────┘
│ a permanent tunnel the Pi keeps open
▼
┌───────────────────────────────┐
│ PI — small box at home │ the internet "exit" + ad-blocker
└───────────────┬───────────────┘
▼
your home internet + your home network
Three kinds of machine:
| Role | What it is | What it does |
|---|---|---|
| Hub | A cheap cloud VM with a public IP (e.g. Oracle Cloud free tier, a $5 VPS) | Listens for connections. Relays traffic. Never exits to the internet itself. |
| Home exit | A Raspberry Pi (or any always-on Linux box) on your home network | Keeps a permanent tunnel to the hub. Provides the internet exit + runs Pi-hole. |
| Clients | Your phone, laptop, etc. | Connect to the hub; their traffic is routed out through home. |
WireGuard is the tunnel technology. Key facts you need:
10.7.0.0/24 (hub = 10.7.0.1, Pi = 10.7.0.2, laptop = 10.7.0.10, phone = 10.7.0.11).AllowedIPs is the important, slightly confusing setting. It means two things at once: (a) which source IPs this peer is allowed to send, and (b) which destinations get routed to this peer. Setting a client's AllowedIPs = 0.0.0.0/0 means "route all my traffic through this tunnel" — that's what makes a full tunnel.Most tutorials make the hub the exit (it masquerades traffic straight out to the internet from the cloud). We deliberately don't. Instead:
You'll set this up in Part 5. Everything before that is the plumbing.
sudo.sudo. Note its home LAN IP (e.g. 192.168.5.145) and your home subnet (e.g. 192.168.4.0/22 — check with ip route).Throughout, replace these placeholders:
| Placeholder | Meaning | Example |
|---|---|---|
<HUB_PUBLIC_IP> | The hub's public IP or DNS name | vpn.example.com |
<HOME_SUBNET> | Your home LAN in CIDR | 192.168.4.0/22 |
<PI_LAN_IF> | The Pi's LAN interface | eth0 |
<HUB_WAN_IF> | The hub's internet interface | ens3 |
Find an interface name with
ip -br addr— it's the one with your real IP, notlo.
On both the hub and the Pi:
bashsudo apt update
sudo apt install -y wireguard
Enable IP forwarding (lets a machine route traffic between interfaces). On both:
bashecho 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Golden rule: a private key is generated on the machine that will use it, and never copied anywhere. You only ever move public keys around.
bashumask 077
wg genkey | sudo tee /etc/wireguard/hub_private.key | wg pubkey | sudo tee /etc/wireguard/hub_public.key
bashumask 077
wg genkey | sudo tee /etc/wireguard/pi_private.key | wg pubkey | sudo tee /etc/wireguard/pi_public.key
wireguard-tools (brew install wireguard-tools on Mac) and run wg genkey | tee laptop.key | wg pubkey > laptop.pub.Now collect the public keys (safe to paste anywhere):
HUB_PUB = contents of the hub's hub_public.keyPI_PUB = contents of the Pi's pi_public.keyLAPTOP_PUB, PHONE_PUB = each client's public keyKeep these handy for the next parts.
Create /etc/wireguard/wg0.conf on the hub:
bashsudo nano /etc/wireguard/wg0.conf
Paste this, filling in the placeholders:
ini[Interface]
Address = 10.7.0.1/24
ListenPort = 51820
PrivateKey = <PASTE hub_private.key CONTENTS>
MTU = 1420
# Don't let wg-quick manage routes; we do it ourselves below.
Table = off
# Allow the tunnel to forward traffic.
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT
# Reach the VPN subnet and the home LAN over the tunnel.
PostUp = ip route replace 10.7.0.0/24 dev wg0; ip route replace <HOME_SUBNET> dev wg0
# THE MAGIC: a separate routing table (200) whose default route goes INTO the tunnel,
# and rules that send client traffic (laptop .10, phone .11) into that table.
# Result: client internet traffic is relayed down to the Pi, not out the hub.
PostUp = ip route replace default dev wg0 table 200; ip route replace 10.7.0.0/24 dev wg0 table 200
PostUp = ip rule add from 10.7.0.10/32 lookup 200 priority 100; ip rule add from 10.7.0.11/32 lookup 200 priority 100
PostDown = ip rule del from 10.7.0.10/32 lookup 200 priority 100 2>/dev/null || true; ip rule del from 10.7.0.11/32 lookup 200 priority 100 2>/dev/null || true
### begin pi ###
[Peer]
PublicKey = <PI_PUB>
# The Pi is the home exit: it may send from home + act as the 0.0.0.0/0 route target.
AllowedIPs = 10.7.0.2/32, <HOME_SUBNET>, 0.0.0.0/0
### end pi ###
### begin laptop ###
[Peer]
PublicKey = <LAPTOP_PUB>
AllowedIPs = 10.7.0.10/32
### end laptop ###
### begin phone ###
[Peer]
PublicKey = <PHONE_PUB>
AllowedIPs = 10.7.0.11/32
### end phone ###
Notice there is no MASQUERADE / NAT rule here. That's on purpose — the hub relays but never exits. The
### begin/end ###comment markers make it easy to find and swap a single peer's key later without disturbing the others.
Bring it up and make it start on boot:
bashsudo systemctl enable --now wg-quick@wg0
sudo wg show wg0 # should show the interface and your peers
Open the firewall for UDP 51820 — both in your cloud provider's security list/firewall and on the host if it runs ufw:
bashsudo ufw allow 51820/udp
Create /etc/wireguard/wgoci.conf on the Pi:
bashsudo nano /etc/wireguard/wgoci.conf
ini[Interface]
Address = 10.7.0.2/24
PrivateKey = <PASTE pi_private.key CONTENTS>
MTU = 1420
# The Pi IS the exit: forward tunnel traffic and masquerade it onto the home LAN.
PostUp = iptables -A FORWARD -i wgoci -j ACCEPT; iptables -A FORWARD -o wgoci -j ACCEPT; iptables -t nat -A POSTROUTING -o <PI_LAN_IF> -j MASQUERADE
PostDown = iptables -D FORWARD -i wgoci -j ACCEPT; iptables -D FORWARD -o wgoci -j ACCEPT; iptables -t nat -D POSTROUTING -o <PI_LAN_IF> -j MASQUERADE
[Peer]
PublicKey = <HUB_PUB>
Endpoint = <HUB_PUBLIC_IP>:51820
# Accept the whole VPN subnet + everything (so it can be the internet exit).
AllowedIPs = 10.7.0.0/24, 0.0.0.0/0
# The Pi dials OUT and keeps the hole punched through the home router's NAT.
PersistentKeepalive = 25
PersistentKeepalive = 25is essential on the home side: your home router has no port forwarding, so the Pi must keep the connection alive from the inside. The hub never initiates to the Pi.
Bring it up:
bashsudo systemctl enable --now wg-quick@wgoci
sudo wg show wgoci # the hub peer should show a handshake within ~30s
bashcurl -sSL https://install.pi-hole.net | bash
Accept the defaults. Afterwards the Pi answers DNS on 10.7.0.2, which clients will use.
The laptop peer block already exists in the hub config from Part 5. If you're adding a new client later, add a block between markers and reload without dropping anyone:
bash# on the hub — reload live, keeping existing tunnels connected:
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'
On the laptop, make a file laptop-home.conf:
ini[Interface]
PrivateKey = <PASTE laptop.key CONTENTS>
Address = 10.7.0.10/24
DNS = 10.7.0.2 # Pi-hole → ad-blocking + no DNS leak
MTU = 1420
[Peer]
PublicKey = <HUB_PUB>
Endpoint = <HUB_PUBLIC_IP>:51820
AllowedIPs = 0.0.0.0/0 # full tunnel: all traffic via home
PersistentKeepalive = 25
Import it:
.conf → Activate.sudo cp laptop-home.conf /etc/wireguard/ && sudo wg-quick up laptop-home.In the WireGuard mobile app, create a tunnel (it makes the key pair). Use the same [Peer] values (hub public key, endpoint, AllowedIPs = 0.0.0.0/0, keepalive 25), Address = 10.7.0.11/24, DNS = 10.7.0.2. Then copy the app's public key into a new ### begin phone ### block on the hub and syncconf as in 7a.
Tip: for phones, the WireGuard app can generate a QR code on the machine that holds the config (
qrencode -t ansiutf8 < phone.conf), and you scan it with the app. That's what QR codes are for here — importing to a phone, not to a computer.
With the client tunnel active:
bash# 1. Handshake — on the hub, the client peer shows a recent handshake + transfer:
sudo wg show wg0
# 2. Exit is HOME, not the hub. Run on the client. It MUST print your home public IP,
# and must NOT be the hub's public IP (that would mean traffic is escaping the hub):
curl -4 ifconfig.me
# 3. Home network reachable over the tunnel:
ping <PI_LAN_IP> # e.g. 192.168.5.145
# 4. DNS via Pi-hole (and confirm the query shows up in the Pi-hole admin log):
dig +short example.com @10.7.0.2
If all four pass, you're done. Toggle the tunnel from the WireGuard app or wg-quick up/down as needed.
AllowedIPs = 10.7.0.0/24, <HOME_SUBNET> (VPN + home LAN only).wg0.conf on the hub, copy it (sudo cp wg0.conf wg0.conf.bak-$(date +%s)), then use wg syncconf to apply live without dropping the other peers.fail2ban and a locked-down SSH config.| Machine | VPN IP | Key role |
|---|---|---|
| Hub (cloud) | 10.7.0.1 | relay + policy routing; no internet exit |
| Pi (home) | 10.7.0.2 | internet exit (masquerade) + Pi-hole DNS |
| Laptop | 10.7.0.10 | full-tunnel client, routed to Pi via table 200 |
| Phone | 10.7.0.11 | full-tunnel client, routed to Pi via table 200 |
Ports: UDP 51820 (hub listens; Pi and clients dial out to it).
DNS: 10.7.0.2 (Pi-hole). Home LAN: <HOME_SUBNET>.