homelab-wireguard-vpn
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHomelab 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
undefinedbash
undefinedInstall 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
undefinedsudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0
undefinedClient Configuration
客户端配置
bash
undefinedbash
undefinedGenerate 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)
undefinedPersistentKeepalive = 25 # Keep NAT hole open (required for mobile clients)
undefinedSplit Tunnel vs Full Tunnel
拆分隧道与全隧道对比
undefinedundefinedSplit 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.
undefinedAllowedIPs = 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.
undefinedKey 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配置
undefinedundefinedpfSense: 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
undefinedWAN → Allow UDP port 51820 inbound (so clients can reach the server)
WireGuard interface → Allow traffic to LAN networks you want reachable
undefinedDDNS (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
undefinedOption 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="
--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
undefinedSign 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="
--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
undefinedTroubleshooting
故障排查
bash
undefinedbash
undefinedCheck 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
undefinedsudo wg-quick down wg0 && sudo wg-quick up wg0
undefinedAnti-Patterns
反模式(错误做法)
undefinedundefinedBAD: 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
undefinedundefinedBest Practices
最佳实践
- Generate a unique keypair per client device — never reuse keys
- Use split tunneling () for mobile
AllowedIPs = <home subnets> - Set on all mobile clients
PersistentKeepalive = 25 - 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 in client configs to get ad blocking over the VPN
DNS = - Rotate the server keypair periodically and update all client configs
- 为每个客户端设备生成唯一的密钥对——切勿重复使用密钥
- 移动设备使用拆分隧道()
AllowedIPs = <家庭子网> - 为所有移动客户端设置
PersistentKeepalive = 25 - 如果ISP分配动态IP,请使用DDNS;将凭据存储在环境文件中,不要直接写在代码里
- 使用范围限定的iptables转发规则(仅针对wg0接口的入站流量),而非全局的FORWARD ACCEPT规则
- 在客户端配置中将Pi-hole的IP设为,以通过VPN实现广告拦截
DNS = - 定期轮换服务器密钥对并更新所有客户端配置
Related Skills
相关技能
- homelab-network-setup
- homelab-vlan-segmentation
- homelab-pihole-dns
- homelab-network-setup
- homelab-vlan-segmentation
- homelab-pihole-dns