homelab-wireguard-vpn

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Homelab WireGuard VPN

家庭实验室WireGuard VPN

WireGuard is a fast, modern VPN protocol. It is the right choice for remote access to a home network — simpler to configure than OpenVPN and faster than most alternatives.
All configuration examples show common setups. Review each command — especially the iptables forwarding rules and key file permissions — before applying them to your system, and make changes in a maintenance window.
WireGuard是一种快速、现代的VPN协议。它是远程访问家庭网络的理想选择——比OpenVPN配置更简单,且比大多数替代方案速度更快。
所有配置示例展示的都是常见部署方式。在将这些命令应用到你的系统之前,请仔细检查每一条命令——尤其是iptables转发规则和密钥文件权限——并在维护窗口内进行更改。

When to Use

适用场景

  • Setting up WireGuard server on a Raspberry Pi, Linux host, pfSense, or router
  • Generating WireGuard keypairs and writing peer config files
  • Configuring remote access from a phone or laptop to a home network
  • Explaining split tunneling (route only home traffic) vs full tunnel (route all traffic)
  • Troubleshooting WireGuard connections that will not come up
  • Automating peer configuration generation for multiple clients
  • 在Raspberry Pi、Linux主机、pfSense或路由器上搭建WireGuard服务器
  • 生成WireGuard密钥对并编写节点配置文件
  • 配置手机或笔记本电脑对家庭网络的远程访问
  • 讲解拆分隧道(仅路由家庭网络流量)与全隧道(路由所有流量)的区别
  • 排查WireGuard连接无法建立的问题
  • 自动化生成多客户端的节点配置

How WireGuard Works

WireGuard工作原理

Your phone (WireGuard client)
    │  Encrypted UDP tunnel (port 51820)
Your home router (WireGuard server — needs a public IP or DDNS)
    Your home network (192.168.1.0/24, NAS, Pi, etc.)

Every device has a keypair (public + private key).
The server knows each client's public key.
The client knows the server's public key + endpoint (IP:port).
Traffic is encrypted end-to-end with no central server or certificate authority.
Your phone (WireGuard client)
    │  Encrypted UDP tunnel (port 51820)
Your home router (WireGuard server — needs a public IP or DDNS)
    Your home network (192.168.1.0/24, NAS, Pi, etc.)

Every device has a keypair (public + private key).
The server knows each client's public key.
The client knows the server's public key + endpoint (IP:port).
Traffic is encrypted end-to-end with no central server or certificate authority.

Server Setup (Linux)

服务器搭建(Linux系统)

bash
undefined
bash
undefined

Install WireGuard

Install WireGuard

sudo apt update && sudo apt install wireguard -y
sudo apt update && sudo apt install wireguard -y

Generate server keypair — create files with private permissions from the start

Generate server keypair — create files with private permissions from the start

sudo mkdir -p /etc/wireguard sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server_private.key' sudo sh -c 'wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key'
sudo mkdir -p /etc/wireguard sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server_private.key' sudo sh -c 'wg pubkey < /etc/wireguard/server_private.key > /etc/wireguard/server_public.key'

Write server config — substitute the actual private key value

Write server config — substitute the actual private key value

Do not store private keys in version control or share them

Do not store private keys in version control or share them

sudo tee /etc/wireguard/wg0.conf << 'EOF' [Interface] Address = 10.8.0.1/24 # VPN subnet — server gets .1 ListenPort = 51820 PrivateKey = <paste_server_private_key_here>
sudo tee /etc/wireguard/wg0.conf << 'EOF' [Interface] Address = 10.8.0.1/24 # VPN subnet — server gets .1 ListenPort = 51820 PrivateKey = <paste_server_private_key_here>

Scoped forwarding rules: allow VPN traffic in/out, not a blanket FORWARD ACCEPT

Scoped forwarding rules: allow VPN traffic in/out, not a blanket FORWARD ACCEPT

PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT PostUp = iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT PostDown = iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT PostUp = iptables -A FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE PostDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT PostDown = iptables -D FORWARD -i eth0 -o wg0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]

Phone — replace with the actual phone public key

Phone — replace with the actual phone public key

PublicKey = <phone_public_key> AllowedIPs = 10.8.0.2/32
[Peer]
PublicKey = <phone_public_key> AllowedIPs = 10.8.0.2/32
[Peer]

Laptop — replace with the actual laptop public key

Laptop — replace with the actual laptop public key

PublicKey = <laptop_public_key> AllowedIPs = 10.8.0.3/32 EOF sudo chmod 600 /etc/wireguard/wg0.conf
PublicKey = <laptop_public_key> AllowedIPs = 10.8.0.3/32 EOF sudo chmod 600 /etc/wireguard/wg0.conf

Replace eth0 with your actual outbound interface name

Replace eth0 with your actual outbound interface name

Check with: ip route show default

Check with: ip route show default

Enable IP forwarding (required for routing traffic through the server)

Enable IP forwarding (required for routing traffic through the server)

echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf sudo sysctl --system
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf sudo sysctl --system

Start WireGuard and enable on boot

Start WireGuard and enable on boot

sudo wg-quick up wg0 sudo systemctl enable wg-quick@wg0
undefined
sudo wg-quick up wg0 sudo systemctl enable wg-quick@wg0
undefined

Client Configuration

客户端配置

bash
undefined
bash
undefined

Generate a unique keypair for each client device

Generate a unique keypair for each client device

Run on the client, or on the server and transfer the private key securely — never in plaintext

Run on the client, or on the server and transfer the private key securely — never in plaintext

umask 077 wg genkey | tee phone_private.key | wg pubkey > phone_public.key
umask 077 wg genkey | tee phone_private.key | wg pubkey > phone_public.key

Client config file (phone_wg0.conf):

Client config file (phone_wg0.conf):

[Interface] PrivateKey = <phone_private_key> Address = 10.8.0.2/32 DNS = 192.168.1.2 # Optional: use Pi-hole for DNS over the tunnel
[Peer] PublicKey = <server_public_key> Endpoint = your-home-ip.ddns.net:51820 # Your public IP or DDNS hostname AllowedIPs = 192.168.1.0/24 # Split tunnel: only home network traffic
[Interface] PrivateKey = <phone_private_key> Address = 10.8.0.2/32 DNS = 192.168.1.2 # Optional: use Pi-hole for DNS over the tunnel
[Peer] PublicKey = <server_public_key> Endpoint = your-home-ip.ddns.net:51820 # Your public IP or DDNS hostname AllowedIPs = 192.168.1.0/24 # Split tunnel: only home network traffic

AllowedIPs = 0.0.0.0/0, ::/0 # Full tunnel: all traffic through VPN

AllowedIPs = 0.0.0.0/0, ::/0 # Full tunnel: all traffic through VPN

PersistentKeepalive = 25 # Keep NAT hole open (required for mobile clients)
undefined
PersistentKeepalive = 25 # Keep NAT hole open (required for mobile clients)
undefined

Split Tunnel vs Full Tunnel

拆分隧道与全隧道对比

undefined
undefined

Split tunnel: AllowedIPs = 192.168.1.0/24

Split tunnel: AllowedIPs = 192.168.1.0/24

Only traffic destined for your home network goes through the VPN. Internet traffic (YouTube, Spotify) goes directly — better performance on mobile. Best for: "I just want to reach my NAS and Pi from anywhere."
Only traffic destined for your home network goes through the VPN. Internet traffic (YouTube, Spotify) goes directly — better performance on mobile. Best for: "I just want to reach my NAS and Pi from anywhere."

Full tunnel: AllowedIPs = 0.0.0.0/0, ::/0

Full tunnel: AllowedIPs = 0.0.0.0/0, ::/0

ALL traffic goes through your home internet connection. Useful for: piggybacking home DNS/Pi-hole ad blocking. Downside: home upload speed becomes your bottleneck everywhere.
ALL traffic goes through your home internet connection. Useful for: piggybacking home DNS/Pi-hole ad blocking. Downside: home upload speed becomes your bottleneck everywhere.

Multi-subnet split tunnel (most common homelab use case):

Multi-subnet split tunnel (most common homelab use case):

AllowedIPs = 192.168.10.0/24, 192.168.20.0/24, 192.168.30.0/24, 10.8.0.0/24 Routes all your VLANs through the tunnel; internet stays direct.
undefined
AllowedIPs = 192.168.10.0/24, 192.168.20.0/24, 192.168.30.0/24, 10.8.0.0/24 Routes all your VLANs through the tunnel; internet stays direct.
undefined

Key Generation and Peer Management

密钥生成与节点管理

python
import subprocess

def generate_keypair() -> tuple[str, str]:
    """Generate a WireGuard keypair. Returns (private_key, public_key)."""
    private = subprocess.check_output(["wg", "genkey"]).decode().strip()
    public = subprocess.run(
        ["wg", "pubkey"], input=private.encode(), capture_output=True
    ).stdout.decode().strip()
    return private, public

def generate_preshared_key() -> str:
    return subprocess.check_output(["wg", "genpsk"]).decode().strip()

def build_client_config(
    client_private_key: str,
    client_vpn_ip: str,       # e.g. "10.8.0.3"
    server_public_key: str,
    server_endpoint: str,     # e.g. "home.example.com:51820"
    allowed_ips: str = "192.168.1.0/24",
    dns: str = "",
) -> str:
    dns_line = f"DNS = {dns}\n" if dns else ""
    return f"""[Interface]
PrivateKey = {client_private_key}
Address = {client_vpn_ip}/32
{dns_line}
[Peer]
PublicKey = {server_public_key}
Endpoint = {server_endpoint}
AllowedIPs = {allowed_ips}
PersistentKeepalive = 25
"""

def build_server_peer_block(
    client_public_key: str,
    client_vpn_ip: str,
    comment: str = "",
) -> str:
    comment_line = f"# {comment}\n" if comment else ""
    return f"""
{comment_line}[Peer]
PublicKey = {client_public_key}
AllowedIPs = {client_vpn_ip}/32
"""
Keep private keys out of source control. If you use this script, write key material to files with mode 600 and never log or print it.
python
import subprocess

def generate_keypair() -> tuple[str, str]:
    """Generate a WireGuard keypair. Returns (private_key, public_key)."""
    private = subprocess.check_output(["wg", "genkey"]).decode().strip()
    public = subprocess.run(
        ["wg", "pubkey"], input=private.encode(), capture_output=True
    ).stdout.decode().strip()
    return private, public

def generate_preshared_key() -> str:
    return subprocess.check_output(["wg", "genpsk"]).decode().strip()

def build_client_config(
    client_private_key: str,
    client_vpn_ip: str,       # e.g. "10.8.0.3"
    server_public_key: str,
    server_endpoint: str,     # e.g. "home.example.com:51820"
    allowed_ips: str = "192.168.1.0/24",
    dns: str = "",
) -> str:
    dns_line = f"DNS = {dns}\n" if dns else ""
    return f"""[Interface]
PrivateKey = {client_private_key}
Address = {client_vpn_ip}/32
{dns_line}
[Peer]
PublicKey = {server_public_key}
Endpoint = {server_endpoint}
AllowedIPs = {allowed_ips}
PersistentKeepalive = 25
"""

def build_server_peer_block(
    client_public_key: str,
    client_vpn_ip: str,
    comment: str = "",
) -> str:
    comment_line = f"# {comment}\n" if comment else ""
    return f"""
{comment_line}[Peer]
PublicKey = {client_public_key}
AllowedIPs = {client_vpn_ip}/32
"""
请勿将私钥存入版本控制系统。如果使用此脚本,请将密钥信息写入权限为600的文件中,切勿记录或打印私钥。

pfSense / OPNsense WireGuard

pfSense/OPNsense下的WireGuard配置

undefined
undefined

pfSense: VPN → WireGuard → Add Tunnel

pfSense: VPN → WireGuard → Add Tunnel

Interface Keys: Generate (creates keypair automatically) Listen Port: 51820 Interface Address: 10.8.0.1/24
Interface Keys: Generate (creates keypair automatically) Listen Port: 51820 Interface Address: 10.8.0.1/24

Add Peer (one per client):

Add Peer (one per client):

Public Key: <client public key> Allowed IPs: 10.8.0.2/32
Public Key: <client public key> Allowed IPs: 10.8.0.2/32

Assign the WireGuard interface:

Assign the WireGuard interface:

Interfaces → Assignments → Add (select wg0) Enable interface, no IP needed (it is set in the tunnel config)
Interfaces → Assignments → Add (select wg0) Enable interface, no IP needed (it is set in the tunnel config)

Firewall rules:

Firewall rules:

WAN → Allow UDP port 51820 inbound (so clients can reach the server) WireGuard interface → Allow traffic to LAN networks you want reachable
undefined
WAN → Allow UDP port 51820 inbound (so clients can reach the server) WireGuard interface → Allow traffic to LAN networks you want reachable
undefined

DDNS (Dynamic DNS) for Home Servers

家庭服务器的DDNS(动态DNS)配置

Most home internet connections have a dynamic IP. Use DDNS so your VPN endpoint stays reachable after an IP change.
bash
undefined
大多数家庭网络的IP是动态的。使用DDNS可以确保IP变更后你的VPN端点仍然可访问。
bash
undefined

Option 1: Cloudflare DDNS — store credentials in a secrets file, not inline

Option 1: Cloudflare DDNS — store credentials in a secrets file, not inline

docker-compose entry using an env file:

docker-compose entry using an env file:

ddns-updater: image: qmcgaw/ddns-updater env_file: ./ddns.env # store zone_id and token here, not in compose restart: unless-stopped
ddns-updater: image: qmcgaw/ddns-updater env_file: ./ddns.env # store zone_id and token here, not in compose restart: unless-stopped

ddns.env (chmod 600, not committed to git):

ddns.env (chmod 600, not committed to git):

SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id

SETTINGS_CLOUDFLARE_ZONE_ID=your_zone_id

SETTINGS_CLOUDFLARE_TOKEN=your_api_token

SETTINGS_CLOUDFLARE_TOKEN=your_api_token

Option 2: DuckDNS (free, simple)

Option 2: DuckDNS (free, simple)

Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org) Store token in /etc/ddns.env (mode 600), then use a small root-owned script:

/usr/local/bin/update-duckdns

#!/bin/sh set -eu . /etc/ddns.env curl --fail --silent --show-error --max-time 10
--get "https://www.duckdns.org/update"
--data-urlencode "domains=myhome"
--data-urlencode "token=${DUCKDNS_TOKEN}"
--data-urlencode "ip="

Cron job:

*/5 * * * * /usr/local/bin/update-duckdns >/dev/null 2>&1
undefined
Sign up at duckdns.org → get a token and subdomain (myhome.duckdns.org) Store token in /etc/ddns.env (mode 600), then use a small root-owned script:

/usr/local/bin/update-duckdns

#!/bin/sh set -eu . /etc/ddns.env curl --fail --silent --show-error --max-time 10
--get "https://www.duckdns.org/update"
--data-urlencode "domains=myhome"
--data-urlencode "token=${DUCKDNS_TOKEN}"
--data-urlencode "ip="

Cron job:

*/5 * * * * /usr/local/bin/update-duckdns >/dev/null 2>&1
undefined

Troubleshooting

故障排查

bash
undefined
bash
undefined

Check WireGuard status and last handshake

Check WireGuard status and last handshake

sudo wg show
sudo wg show

If "latest handshake" is never or very old, the tunnel is not connected.

If "latest handshake" is never or very old, the tunnel is not connected.

Check:

Check:

1. Is UDP port 51820 open on the router/firewall?

1. Is UDP port 51820 open on the router/firewall?

sudo ufw status # or check pfSense/UniFi firewall rules
sudo ufw status # or check pfSense/UniFi firewall rules

2. Is the server public key in the client config correct?

2. Is the server public key in the client config correct?

sudo wg show wg0 public-key # Compare to what is in the client config
sudo wg show wg0 public-key # Compare to what is in the client config

3. Is IP forwarding enabled on the server?

3. Is IP forwarding enabled on the server?

cat /proc/sys/net/ipv4/ip_forward # Should be 1
cat /proc/sys/net/ipv4/ip_forward # Should be 1

4. Does the client AllowedIPs cover the IP you are trying to reach?

4. Does the client AllowedIPs cover the IP you are trying to reach?

If AllowedIPs = 192.168.1.0/24 and you are trying to reach 192.168.3.5, it will not route.

If AllowedIPs = 192.168.1.0/24 and you are trying to reach 192.168.3.5, it will not route.

Check kernel logs for WireGuard errors

Check kernel logs for WireGuard errors

dmesg | grep wireguard
dmesg | grep wireguard

Restart WireGuard

Restart WireGuard

sudo wg-quick down wg0 && sudo wg-quick up wg0
undefined
sudo wg-quick down wg0 && sudo wg-quick up wg0
undefined

Anti-Patterns

反模式(错误做法)

undefined
undefined

BAD: Storing private keys in version control or sharing them

BAD: Storing private keys in version control or sharing them

Private keys are equivalent to passwords — never commit them to git

Private keys are equivalent to passwords — never commit them to git

BAD: Using AllowedIPs = 0.0.0.0/0 on mobile without considering the impact

BAD: Using AllowedIPs = 0.0.0.0/0 on mobile without considering the impact

Full tunnel routes all mobile traffic through your home upload — usually slow

Full tunnel routes all mobile traffic through your home upload — usually slow

BAD: Not setting PersistentKeepalive on mobile clients

BAD: Not setting PersistentKeepalive on mobile clients

Mobile clients behind NAT drop idle tunnels without it

Mobile clients behind NAT drop idle tunnels without it

BAD: Opening port 51820 in the firewall but forgetting IP forwarding on the server

BAD: Opening port 51820 in the firewall but forgetting IP forwarding on the server

Tunnel connects but no traffic routes — confusing to debug

Tunnel connects but no traffic routes — confusing to debug

BAD: Sharing a keypair across multiple client devices

BAD: Sharing a keypair across multiple client devices

Each device must have its own unique keypair — shared keys break the security model

Each device must have its own unique keypair — shared keys break the security model

BAD: Using a broad "FORWARD ACCEPT" iptables rule

BAD: Using a broad "FORWARD ACCEPT" iptables rule

Scope forwarding rules to the wg0 interface and direction only

Scope forwarding rules to the wg0 interface and direction only

undefined
undefined

Best Practices

最佳实践

  • Generate a unique keypair per client device — never reuse keys
  • Use split tunneling (
    AllowedIPs = <home subnets>
    ) for mobile
  • Set
    PersistentKeepalive = 25
    on all mobile clients
  • Use DDNS if your ISP assigns a dynamic IP; store credentials in env files, not inline
  • Use scoped iptables forwarding rules (inbound on wg0 only) rather than a blanket FORWARD ACCEPT
  • Add Pi-hole's IP as
    DNS =
    in client configs to get ad blocking over the VPN
  • Rotate the server keypair periodically and update all client configs
  • 为每个客户端设备生成唯一的密钥对——切勿重复使用密钥
  • 移动设备使用拆分隧道(
    AllowedIPs = <家庭子网>
  • 为所有移动客户端设置
    PersistentKeepalive = 25
  • 如果ISP分配动态IP,请使用DDNS;将凭据存储在环境文件中,不要直接写在代码里
  • 使用范围限定的iptables转发规则(仅针对wg0接口的入站流量),而非全局的FORWARD ACCEPT规则
  • 在客户端配置中将Pi-hole的IP设为
    DNS =
    ,以通过VPN实现广告拦截
  • 定期轮换服务器密钥对并更新所有客户端配置

Related Skills

相关技能

  • homelab-network-setup
  • homelab-vlan-segmentation
  • homelab-pihole-dns
  • homelab-network-setup
  • homelab-vlan-segmentation
  • homelab-pihole-dns