自建私有VPN网络:Headscale完整部署指南
引言
在远程办公和分布式团队日益普遍的今天,搭建一个安全、私密的虚拟专用网络(VPN)成为许多开发者和企业的刚需。Tailscale 基于 WireGuard 协议,提供了优秀的组网体验,但其控制服务器闭源且依赖官方服务。
Headscale 作为 Tailscale 控制服务器的开源实现,让你能够完全掌控自己的 VPN 网络。本文将完整记录从零开始部署 Headscale 的全过程,涵盖服务端、Web 管理界面、Nginx 反向代理、HTTPS 证书、DERP 中继以及客户端连接的所有配置细节。
部署方式说明:Headscale 官方推荐使用二进制或 DEB 包部署,Docker 部署由社区维护。本文采用 Docker Compose 方式,这是目前最便捷且可维护性最好的方案。
第一部分:架构概览
1.1 整体架构
┌───────────────────────────────┐
│ 你的服务器 │
│ │
│ ┌───────────────────────────┐ │
│ │ Nginx (443/80) │ │
│ │ / → 127.0.0.1:8080 (Headscale) │ │
│ │ /admin/ → 127.0.0.1:3001 (Headplane) │ │
│ └───────────────────────────┘ │
│ ↓ │
│ ┌──────────┐ ┌───────────┐ │
│ │ Headscale 容器 │ ←→ │ Headplane 容器 │ │
│ │ 监听 8080 │ │ 监听 3000 │ │
│ │ │ │ │ │
│ │ SQLite 数据库 │ │ 通过容器名访问 │ │
│ │ Noise 私钥 │ │ headscale:8080 │ │
│ │ DERP 私钥 │ │ │ │
│ └──────────┘ └───────────┘ │
│ │
│ DERP 服务 (443) + STUN (3478/UDP) │
│ 公网 IP: 已配置 │
└───────────────────────────────┘
↓
┌───────┴────────┐
│ │
客户端 A (P2P直连) 客户端 B (DERP中继)
延迟 4ms 延迟 300ms+
1.2 组件职责
| 组件 | 职责 | 端口 |
|---|---|---|
| Headscale | 控制服务器,管理设备注册、IP 分配、ACL 策略 | 8080 (内部) |
| Headplane | Web 管理界面 | 3000 (内部) |
| Nginx | 反向代理,HTTPS 终结,WebSocket 转发 | 443, 80 |
| DERP | 中继服务,P2P 失败时的保底通道 | 443, 3478/UDP |
| SQLite | 数据持久化 | — |
第二部分:环境准备
2.1 服务器要求
- 操作系统:Ubuntu 20.04/22.04 LTS
- Docker:20.10+
- Docker Compose:v2+
- Nginx:1.18+
- 公网 IP:必需(用于 DERP 和客户端连接)
- 域名:一个解析到服务器 IP 的域名
2.2 域名规划
| 域名 | 用途 |
|---|---|
<你的域名> | Headscale 核心服务(客户端连接用) |
<你的基础域名> | MagicDNS 后缀(设备全名) |
注意:MagicDNS 的
base_domain不能和server_url的域名相同。
2.3 防火墙端口
| 端口 | 协议 | 用途 | 是否必需 |
|---|---|---|---|
| 443 | TCP | HTTPS + DERP | ✅ 必需 |
| 80 | TCP | HTTP(Certbot 验证 + 跳转) | ✅ 必需 |
| 3478 | UDP | STUN(NAT 穿透辅助) | ⚠️ 建议开放 |
| 41641 | UDP | WireGuard 直连(可选) | ⚠️ 按需 |
关于 UDP 3478 (STUN):这是 Headscale 内置 DERP 服务器自带的 STUN 服务端口。开放它可以帮助客户端发现公网地址,尝试 NAT 穿透,提高 P2P 直连成功率。
关于 UDP 41641 (WireGuard):这是 Tailscale/Headscale 用于建立 P2P 加密隧道的默认端口。不是必须手动开放,客户端会自动协商。
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp
2.4 IPv6 配置说明
如果你的服务器有公网 IPv6 地址,可以在 DERP 配置中填写 ipv6 字段,这有助于提升连接稳定性。
关键区分:
- 服务端的 IPv6:需要在
config.yaml的derp.server.ipv6字段中填写服务器的真实公网 IPv6 地址 - 客户端的 IPv6:由客户端设备从运营商自动获取,你不需要也无法手动配置。Headscale 会自动处理客户端 IPv6 地址的变化,VPN 虚拟 IP(
100.64.0.x)保持不变
第三部分:服务端部署
3.1 目录结构
mkdir -p ~/headscale/{config,data}
mkdir -p ~/headscale/config/headplane
cd ~/headscale
最终目录结构:
~/headscale/
├── docker-compose.yml
├── config/
│ ├── config.yaml # Headscale 配置
│ ├── policy.hujson # ACL 策略
│ └── headplane/
│ └── config.yaml # Headplane 配置
└── data/
├── db.sqlite # 数据库
├── noise_private.key # Noise 私钥
└── derp_server_private.key # DERP 私钥
3.2 docker-compose.yml 详解
services:
headscale:
image: headscale/headscale:latest
container_name: headscale
restart: unless-stopped
volumes:
- ./config:/etc/headscale
- ./data:/var/lib/headscale
ports:
- "127.0.0.1:8080:8080"
- "3478:3478/udp"
command: serve
networks:
- headscale-net
headplane:
image: ghcr.io/tale/headplane:latest
container_name: headplane
restart: unless-stopped
volumes:
- ./config/headplane:/etc/headplane
ports:
- "127.0.0.1:3001:3000"
networks:
- headscale-net
networks:
headscale-net:
driver: bridge
| 配置项 | 说明 |
|---|---|
image | 使用官方最新镜像 |
container_name | 容器名,方便 docker logs |
restart: unless-stopped | 服务器重启后自动启动 |
./config:/etc/headscale | 配置文件目录映射 |
./data:/var/lib/headscale | 数据目录映射(数据库、私钥) |
127.0.0.1:8080:8080 | 只绑定本机,外部通过 Nginx 访问 |
3478:3478/udp | STUN 端口,需要直接暴露 |
command: serve | 必须显式指定,否则容器不启动服务 |
headscale-net | 自定义网络,让 Headplane 能用容器名访问 Headscale |
为什么端口只绑定 127.0.0.1?
安全性考虑:不直接暴露到公网,所有流量必须经过 Nginx(HTTPS + 证书校验)。统一入口让 Nginx 负责 SSL 终结、WebSocket 转发和路径分发。
为什么需要自定义网络?
Headplane 需要通过 http://headscale:8080 访问 Headscale,只有同一网络内的容器才能用容器名互相解析。
3.3 config/config.yaml 详解
# ============================================
# 基础地址
# ============================================
server_url: https://<你的域名>
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
| 字段 | 含义 | 注意 |
|---|---|---|
server_url | 客户端连接的公开地址 | 必须 HTTPS,不能写 127.0.0.1 |
listen_addr | Headscale 监听地址 | 0.0.0.0 才能被 Headplane 访问 |
metrics_listen_addr | Prometheus 指标端点 | 保持内部访问即可,不要对公网开放 |
为什么 server_url 不能写 127.0.0.1?
这个地址会被写入客户端配置,客户端需要用它来连接。127.0.0.1 是容器内部的,客户端根本访问不到。
# ============================================
# 数据库
# ============================================
database:
type: sqlite3
sqlite:
path: /var/lib/headscale/db.sqlite
| 字段 | 含义 |
|---|---|
type | 数据库类型,可选 sqlite3 或 postgres |
sqlite.path | SQLite 数据库文件路径 |
为什么用 SQLite? 个人/小团队(<100 节点)完全够用,零维护。PostgreSQL 只在超大规模或需要高可用时才考虑,且 Headscale 官方对 PostgreSQL 的支持仅为遗留目的。
# ============================================
# IP 地址段
# ============================================
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
| 字段 | 含义 |
|---|---|
v4 | VPN IPv4 地址段,必须是 100.64.0.0/10 子集 |
v6 | VPN IPv6 地址段,必须是 Tailscale ULA 范围 |
这两个地址段是 Tailscale 协议硬性规定的,不能随便改。设备看到的 100.64.0.1、100.64.0.2 就是从这里来的。
# ============================================
# Noise 协议私钥(新版必需)
# ============================================
noise:
private_key_path: /var/lib/headscale/noise_private.key
| 字段 | 含义 |
|---|---|
private_key_path | Tailscale v2 协议(Noise)的加密私钥 |
首次启动时如果文件不存在,会自动生成。不要删这个文件,删了所有客户端都要重新注册。
# ============================================
# DERP 中继
# ============================================
derp:
server:
enabled: true
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
ipv4: <你的服务器公网IPv4>
ipv6: <你的服务器公网IPv6>
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
urls:
- https://controlplane.tailscale.com/derpmap/default
| 字段 | 含义 |
|---|---|
enabled | 是否启用内置 DERP 中继 |
region_id | 自定义区域 ID,避免和公共 DERP 冲突(使用 900+) |
ipv4 | 服务器公网 IP,客户端用它连接你的 DERP |
ipv6 | 服务器公网 IPv6(可选) |
stun_listen_addr | STUN 服务地址,帮助 NAT 穿透 |
private_key_path | DERP 加密私钥,自动生成 |
urls | 公共 DERP 列表,作为备用 |
DERP 的作用:当两台设备无法 P2P 直连时,流量通过 DERP 中转。它是“保底方案”,不是首选。
# ============================================
# DNS / MagicDNS
# ============================================
dns:
magic_dns: true
base_domain: <你的基础域名>
override_local_dns: true
nameservers:
global:
# IPv4
- 1.1.1.1
- 1.0.0.1
# IPv6(可选,推荐双栈环境添加)
- 2606:4700:4700::1111
- 2606:4700:4700::1001
| 字段 | 含义 |
|---|---|
magic_dns | 是否启用 MagicDNS(用设备名代替 IP) |
base_domain | MagicDNS 域名后缀,设备全名变成 <设备名>.<你的基础域名> |
override_local_dns | 是否覆盖客户端本地 DNS 设置 |
nameservers.global | 客户端使用的上游 DNS 服务器 |
base_domain 的要求:
- 不能和
server_url的域名相同 - 必须是 FQDN(完整域名),不带结尾的点
效果:ping <设备名> 自动解析成 <设备名>.<你的基础域名> → 100.64.0.x。
关于 DNS 配置:nameservers.global 中的 DNS 地址是下发给客户端的,用于客户端解析外网域名。如果服务器有 IPv6,也可以在这里添加 IPv6 的 DNS 地址。
# ============================================
# ACL 策略
# ============================================
policy:
mode: database
path: /etc/headscale/policy.hujson
| 字段 | 含义 |
|---|---|
mode | file 或 database。database 模式支持通过 Headplane 界面编辑 |
path | ACL 策略文件路径(file 模式时使用) |
# ============================================
# 日志
# ============================================
log:
level: info
format: text
| 字段 | 含义 |
|---|---|
level | 日志级别:debug/info/warn/error |
format | text(人类可读)或 json(机器解析) |
3.4 config/policy.hujson 详解
全部允许(默认):
{
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
]
}
| 字段 | 含义 |
|---|---|
action | 固定 accept(允许) |
src | 来源设备,* 表示所有 |
dst | 目标设备:端口,*:* 表示所有设备和端口 |
精细化控制示例(基于标签):
{
"tagOwners": {
"tag:dev": ["default"],
"tag:server": ["default"]
},
"acls": [
{
"action": "accept",
"src": ["tag:dev"],
"dst": ["tag:server:22,80,443"]
}
]
}
标签体系比用户体系更灵活,可以跨用户管理设备访问权限。
3.5 config/headplane/config.yaml 详解
headscale:
url: http://headscale:8080
server:
listen: :3000
cookie_secret: 32位随机字符串
allow_registration: true
oauth:
enabled: false
| 字段 | 含义 |
|---|---|
headscale.url | Headplane 访问 Headscale 的地址,用容器名 |
server.listen | Headplane 监听端口,容器内 3000 |
server.cookie_secret | 必须正好 32 位字符串,用于加密会话 Cookie |
server.allow_registration | 是否允许注册 Headplane 账户 |
oauth.enabled | 是否启用 OIDC 单点登录 |
cookie_secret 生成方法:
openssl rand -hex 16
输出正好 32 个字符。
3.6 启动服务
cd ~/headscale
docker compose up -d
docker compose ps
docker logs headscale
第四部分:Nginx 反向代理配置
4.1 HTTP 版本(Certbot 前)
创建 /etc/nginx/sites-available/headscale:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name <你的域名>;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
location /admin/ {
proxy_pass http://127.0.0.1:3001/admin/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /health {
proxy_pass http://127.0.0.1:8080/health;
access_log off;
proxy_set_header Host $server_name;
}
}
4.2 启用配置
sudo ln -s /etc/nginx/sites-available/headscale /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
4.3 Certbot 自动配置 HTTPS
sudo apt update
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d <你的域名>
Certbot 会自动完成:获取 SSL 证书、添加 listen 443 ssl、填入证书路径、添加 HTTP → HTTPS 跳转、引入安全配置文件。
4.4 关键配置说明
| 配置 | 作用 |
|---|---|
map $http_upgrade $connection_upgrade | WebSocket 必需,少了 Headscale 客户端连不上 |
proxy_http_version 1.1 | WebSocket 需要 HTTP/1.1 |
proxy_buffering off | WebSocket 实时通信必需 |
proxy_set_header Host $server_name | 保持原始域名,Headscale 用它校验 |
location /admin/ | Headplane 路径,proxy_pass 末尾要带 /admin/ |
location = /health | 精确匹配,只处理 /health 请求 |
第五部分:用户与密钥管理
5.1 创建用户
Headscale 中的用户是设备归属和权限隔离的基本单位:
docker exec headscale headscale users create default
5.2 生成 API Key
API Key 用于登录 Headplane 管理界面:
docker exec headscale headscale apikeys create --expiration 999d
5.3 生成预授权密钥
预授权密钥用于客户端自动注册:
docker exec headscale headscale preauthkeys create --user default --reusable --expiration 365d
5.4 查看管理
docker exec headscale headscale users list
docker exec headscale headscale nodes list
docker exec headscale headscale nodes list --user default
第六部分:客户端连接
6.1 Linux 客户端
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://<你的域名> --authkey <预授权密钥>
Tailscale 客户端会自动与 Headscale 控制服务器注册,交换 WireGuard 密钥,并建立 P2P 加密连接。
6.2 Windows 客户端
命令行方式(管理员 PowerShell):
tailscale up --login-server https://<你的域名> --authkey <预授权密钥>
注册表方式(免命令行):
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN]
"LoginURL"="https://<你的域名>"
6.3 OpenWrt 客户端
wget -O /usr/sbin/install.sh https://ghfast.top/https://raw.githubusercontent.com/GuNanOvO/openwrt-tailscale/main/install.sh && chmod +x /usr/sbin/install.sh && /usr/sbin/install.sh
tailscale up --login-server https://<你的域名> --authkey <预授权密钥>
第七部分:网络功能验证
7.1 DERP 中继验证
docker logs headscale | grep -i derp
tailscale debug derp-map
7.2 P2P 直连测试
tailscale ping <目标设备IP>
输出示例:
pong from <设备名> (100.64.0.2) via DERP(sfo) in 336ms # 走中继
pong from <设备名> (100.64.0.2) via [xxxx:...]:52306 in 5ms # P2P 直连
tailscale ping --verbose 可以显示是 DERP 中继还是直连路径。
7.3 MagicDNS 验证
nslookup <设备名>
# 应返回 <设备名>.<你的基础域名> -> 100.64.0.x
7.4 NAT 类型检测
tailscale netcheck
报告 NAT 类型、DERP 延迟、IPv4/IPv6 能力。
第八部分:ACL 策略详解
8.1 ACL 的作用
ACL 控制哪些设备能访问哪些资源,是 Headscale 的核心安全功能。Headscale 支持两种策略存储模式:
- file 模式:从 JSON 文件只读加载,适合简单场景
- database 模式:通过 API 和 Headplane 界面编辑,适合生产环境(推荐)
8.2 策略文件结构
{
"groups": {},
"tagOwners": {},
"acls": [],
"ssh": [],
"autoApprovers": {}
}
8.3 常用写法
| 写法 | 含义 |
|---|---|
"*" | 所有设备 |
"default:*" | default 用户下的所有设备 |
"tag:server" | 打了 tag:server 标签的设备 |
"100.64.0.1" | 具体 IP |
"*:*" | 所有端口 |
"default:*:22" | default 用户下所有设备的 SSH 端口 |
8.4 典型场景
场景 1:全部允许(默认)
{
"acls": [
{"action": "accept", "src": ["*"], "dst": ["*:*"]}
]
}
场景 2:开发人员只能访问开发服务器
{
"tagOwners": {
"tag:dev": ["default"],
"tag:dev-server": ["default"]
},
"acls": [
{"action": "accept", "src": ["tag:dev"], "dst": ["tag:dev-server:*"]}
]
}
第九部分:故障排查
9.1 常用诊断命令
curl https://<你的域名>/health
docker logs headscale | grep -E "derp|STUN"
docker logs headplane | tail -20
sudo nginx -t
sudo systemctl status nginx
sudo ss -tlnp | grep -E "8080|3001|443|3478"
sudo ufw status
9.2 常见问题
| 问题 | 原因 | 解决 |
|---|---|---|
| 客户端连接超时 | 防火墙未放行 | 放行 443/TCP、3478/UDP |
| 证书错误 | 域名未解析 | 检查 DNS 解析 |
| Headplane 401 | API Key 错误 | 重新生成 API Key |
| WebSocket 失败 | Nginx 缺少 Upgrade 头 | 添加 map 和 proxy_set_header |
| 容器反复重启 | 配置字段错误 | 查看 docker logs 定位 |
第十部分:部署完成清单
| 项目 | 状态 |
|---|---|
| Headscale 服务 | ✅ 运行中 |
| Headplane 管理界面 | ✅ 运行中 |
| Nginx 反向代理 | ✅ 运行中 |
| SSL 证书 | ✅ 有效 |
| DERP 中继 | ✅ 已启用 |
| STUN (UDP 3478) | ✅ 已开放 |
| 用户 | ✅ default |
| 客户端连接 | ✅ 已验证 |
| P2P 直连 | ✅ IPv6 直连 |
| 管理界面登录 | ✅ API Key 登录 |
结语
通过以上步骤,我们搭建了一个完全自控的私有 VPN 网络。Headscale 作为开源控制服务器,配合 Headplane 管理界面,提供了与 Tailscale 官方服务相当的使用体验,同时让你完全掌握数据主权和网络配置。
关于官方支持的重要说明:Headscale 官方推荐使用二进制或 DEB 包部署,Docker 部署由社区维护并提供支持。本文采用的 Docker Compose 方案是目前最便捷的选择,但请留意社区镜像的更新情况。
这套方案适合希望摆脱第三方服务依赖的开发者、需要精细控制 VPN 网络的企业,以及对数据隐私有较高要求的个人用户。随着对 ACL 策略、子网路由、出口节点等高级功能的深入探索,可以构建出满足各种复杂需求的私有网络架构。