MC.Back to résumé ↗

Build a "Home-Exit" WireGuard VPN — From Scratch

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.

Contents

Part 1 — What this is and why you'd want it

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 core idea

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:

RoleWhat it isWhat it does
HubA 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 exitA Raspberry Pi (or any always-on Linux box) on your home networkKeeps a permanent tunnel to the hub. Provides the internet exit + runs Pi-hole.
ClientsYour phone, laptop, etc.Connect to the hub; their traffic is routed out through home.

How WireGuard fits in

WireGuard is the tunnel technology. Key facts you need:

The clever part: exit through home, not the hub

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.


Part 2 — What you need before you start

Throughout, replace these placeholders:

PlaceholderMeaningExample
<HUB_PUBLIC_IP>The hub's public IP or DNS namevpn.example.com
<HOME_SUBNET>Your home LAN in CIDR192.168.4.0/22
<PI_LAN_IF>The Pi's LAN interfaceeth0
<HUB_WAN_IF>The hub's internet interfaceens3

Find an interface name with ip -br addr — it's the one with your real IP, not lo.


Part 3 — Install WireGuard on the hub and the Pi

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

Part 4 — Generate the keys

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.

On the hub

bashumask 077
wg genkey | sudo tee /etc/wireguard/hub_private.key | wg pubkey | sudo tee /etc/wireguard/hub_public.key

On the Pi

bashumask 077
wg genkey | sudo tee /etc/wireguard/pi_private.key | wg pubkey | sudo tee /etc/wireguard/pi_public.key

On each client (phone, laptop)

Now collect the public keys (safe to paste anywhere):

Keep these handy for the next parts.


Part 5 — Configure the hub (the meeting point + policy routing)

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

Part 6 — Configure the Pi (the home exit)

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 = 25 is 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

(Recommended) Install Pi-hole for network-wide ad-blocking

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.


Part 7 — Add a client (laptop example)

7a. Tell the hub about the client's public key

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)'

7b. Create the client config

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:

7c. Phone

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.


Part 8 — Verify it works

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.


Part 9 — Security & operational notes


Quick reference — the addressing plan

MachineVPN IPKey role
Hub (cloud)10.7.0.1relay + policy routing; no internet exit
Pi (home)10.7.0.2internet exit (masquerade) + Pi-hole DNS
Laptop10.7.0.10full-tunnel client, routed to Pi via table 200
Phone10.7.0.11full-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>.