跳至正文
老丹的足迹 —— 代码写给机器,游记写给自己,感悟写给时间
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面
老丹的足迹 老丹的足迹
老丹的足迹 老丹的足迹
  • 首页
  • 示例页面
  • 首页
  • 示例页面

自建私有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 (内部)
HeadplaneWeb 管理界面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 防火墙端口

端口协议用途是否必需
443TCPHTTPS + DERP✅ 必需
80TCPHTTP(Certbot 验证 + 跳转)✅ 必需
3478UDPSTUN(NAT 穿透辅助)⚠️ 建议开放
41641UDPWireGuard 直连(可选)⚠️ 按需

关于 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/udpSTUN 端口,需要直接暴露
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_addrHeadscale 监听地址0.0.0.0 才能被 Headplane 访问
metrics_listen_addrPrometheus 指标端点保持内部访问即可,不要对公网开放

为什么 server_url 不能写 127.0.0.1?

这个地址会被写入客户端配置,客户端需要用它来连接。127.0.0.1 是容器内部的,客户端根本访问不到。

# ============================================
# 数据库
# ============================================
database:
  type: sqlite3
  sqlite:
    path: /var/lib/headscale/db.sqlite
字段含义
type数据库类型,可选 sqlite3 或 postgres
sqlite.pathSQLite 数据库文件路径

为什么用 SQLite? 个人/小团队(<100 节点)完全够用,零维护。PostgreSQL 只在超大规模或需要高可用时才考虑,且 Headscale 官方对 PostgreSQL 的支持仅为遗留目的。

# ============================================
# IP 地址段
# ============================================
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
字段含义
v4VPN IPv4 地址段,必须是 100.64.0.0/10 子集
v6VPN 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_pathTailscale 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_addrSTUN 服务地址,帮助 NAT 穿透
private_key_pathDERP 加密私钥,自动生成
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_domainMagicDNS 域名后缀,设备全名变成 <设备名>.<你的基础域名>
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
字段含义
modefile 或 database。database 模式支持通过 Headplane 界面编辑
pathACL 策略文件路径(file 模式时使用)
# ============================================
# 日志
# ============================================
log:
  level: info
  format: text
字段含义
level日志级别:debug/info/warn/error
formattext(人类可读)或 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.urlHeadplane 访问 Headscale 的地址,用容器名
server.listenHeadplane 监听端口,容器内 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_upgradeWebSocket 必需,少了 Headscale 客户端连不上
proxy_http_version 1.1WebSocket 需要 HTTP/1.1
proxy_buffering offWebSocket 实时通信必需
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 401API 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 策略、子网路由、出口节点等高级功能的深入探索,可以构建出满足各种复杂需求的私有网络架构。

作者

老丹

关注我
其他文章
上一个

constexpr:C++编译期计算的基石

下一个

C++ 智能指针完全指南:从入门到工程实战

关于博主

    老丹是一名C/C++后台开发工程师,信奉“无抽象不设计,无性能不生产”。

  • 技术栈:Modern C++、Linux环境编程、多线程/并发、网络编程等。
  • 信条:能用constexpr解决的问题绝不拖到运行时,能靠RAII避免的泄漏绝不写析构。
  • 正在填坑:从解封装到渲染的C++全链路实现,正在驯服FFmpeg与H.264/H.265。
  • 输出原则:这里的每一段代码都经过-Wall -Wextra -Werror -O2的洗礼。

近期文章

  • 在 Windows 里养一只 Linux:WSL 完整指南 2026年10月24日
  • Paho MQTT C 与 C++ 客户端库完全指南:从概念到 Ubuntu 26.04 下的编译、安装与使用 2026年10月23日
  • Mosquitto $SYS 主题详解:Broker 自监控指南 2026年10月23日
  • 应用级 MQTT 服务搭建完全指南:Mosquitto + 宿主机 Nginx 2026年10月22日
  • OpenWrt 固件签名三剑客:usign、ucert 与 fwtool 深度解析 2026年10月21日

文章分类

  • C/C++开发 (22)
  • Docker容器 (5)
  • Linux工具包 (21)
  • Linux服务配置 (53)
  • Linux系统 (16)
  • MQTT技术 (6)
  • OpenWrt路由 (5)
  • Shell脚本 (3)
  • 代码管理 (1)
  • 安防技术 (4)
  • 数据安全 (36)
  • 网络协议 (25)
  • 计算机理论 (23)
  • 问题解决 (3)
  • 防火墙 (6)
  • 音视频技术 (6)
联系我们:📍 地址:中国·广东省深圳市   |   ✉️ 邮箱:support@tanglinux.com   |   💬 QQ:870866607
版权所有:老丹的足迹粤ICP备2026061170号-1       公安备案图标 粤公网安备44030002013274号