彻底私有化你的内网:Headscale + 内置 DERP 全自托管 VPN 搭建指南

系列导航 本文是「自托管 VPN」系列第 1 篇,介绍完整部署流程。

# Headscale 全自托管 VPN ## 架构组件 ### Headscale:开源控制面 ### 内置 DERP:中继服务器 ### tailscaled:官方开源客户端 ### Caddy:TLS 反向代理 ## 网络原理 ### WireGuard 点对点直连 ### STUN 探测公网 IP ### DERP 兜底中继(NAT 穿透失败时) ### MagicDNS 自动域名解析 ## 部署步骤 ### 1. DNS 解析配置 ### 2. 安装 Headscale 二进制 ### 3. 编写配置文件 ### 4. 配置 Caddy 反代 ### 5. 创建 systemd 服务 ### 6. 创建用户与注册设备 ## 客户端接入 ### Linux(tailscale CLI) ### macOS(Tailscale App + 自定义控制面) ### iOS / Android ### Windows ## 日常管理 ### 查看设备列表 ### 撤销设备 ### ACL 访问控制 ### Exit Node(VPN 出口)

架构总览

这套方案的核心思想是:用开源的 Headscale 替代 Tailscale 的官方控制服务器,所有设备元数据、路由策略都只存在于你自己的服务器上;流量走 WireGuard 直连,NAT 穿透失败时通过自建 DERP 中继兜底,全程不经过任何第三方。

flowchart LR A[设备 A\n手机/电脑] -->|1 注册/心跳| B[hs.k330.com\nHeadscale 控制面] C[设备 B\n家里 NAS] -->|1 注册/心跳| B A <-->|2 WireGuard 直连\n延迟最低| C A -->|3 NAT 穿透失败时| D[hs.k330.com/derp\n内置 DERP 中继] C -->|3 NAT 穿透失败时| D B -->|分发 DERP 地图\n分配 VPN IP| A B -->|分发 DERP 地图\n分配 VPN IP| C
角色 软件 运行位置
控制面 Headscale v0.29.2 公网服务器(本机)
DERP 中继 Headscale 内置 同上,/derp 路径
STUN Headscale 内置 UDP 3478
TLS 终止 Caddy 同上,443 端口
客户端 tailscale(官方开源) 各接入设备

为什么选内置 DERP 而非单独部署 derper Headscale v0.22+ 已内置 DERP 服务器,和控制面共用同一进程,零额外部署成本。在同一台服务器资源紧张时是首选。如需多地 DERP 节点,再单独部署 derper 扩展。


一、前置条件

服务器要求

项目 最低要求
公网 IP 必须,固定或动态均可
内存 64 MB(Headscale 本身极轻量)
系统 Linux(Debian/Ubuntu/CentOS 均可)
开放端口 TCP 443(由 Caddy 统一管理)、UDP 3478(STUN)、UDP 41641(WireGuard 直连,可选)

DNS 配置

在你的 DNS 服务商(如 Cloudflare)添加两条 A 记录

子域名 类型 说明
hs.k330.com A 你的服务器公网 IP Headscale 控制面 + DERP
derp.k330.com A 你的服务器公网 IP 预留,用于未来独立 DERP 节点

Cloudflare 用户:关闭橙色云图标(Proxy),改为 DNS only(灰色),否则 UDP STUN 流量会被 CF 拦截。


二、安装 Headscale

# 下载预编译二进制(x86_64)
curl -fsSL -o /tmp/headscale \
  "https://github.com/juanfont/headscale/releases/download/v0.29.2/headscale_0.29.2_linux_amd64"
chmod +x /tmp/headscale
sudo mv /tmp/headscale /usr/local/bin/headscale

# 验证
headscale version
# 输出:headscale version v0.29.2

# 创建专用系统用户(无 shell、无家目录登录权限)
sudo useradd -r -m -d /var/lib/headscale -s /usr/bin/nologin headscale

# 创建所需目录
sudo mkdir -p /etc/headscale /var/lib/headscale /var/run/headscale
sudo chown headscale:headscale /var/lib/headscale /var/run/headscale

三、编写配置文件

主配置 /etc/headscale/config.yaml

sudo tee /etc/headscale/config.yaml > /dev/null << 'EOF'
# 客户端实际访问的 URL(必须 HTTPS,Caddy 提供 TLS)
server_url: https://hs.k330.com

# Headscale 内部监听地址(Caddy 反代到这里)
listen_addr: 127.0.0.1:8085
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: true

# 信任 Caddy 传来的 X-Forwarded-For
trusted_proxies:
  - 127.0.0.1/32

noise:
  private_key_path: /var/lib/headscale/noise_private.key

# VPN 网段(Tailscale 协议规定必须在此范围内)
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48
  allocation: sequential

# 内置 DERP 中继
derp:
  server:
    enabled: true
    region_id: 900
    region_code: cn-self
    region_name: "自建中继(hs.k330.com)"
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    ipv4: 104.160.46.59   # ← 改为你的服务器公网 IP
    ipv6: ""
  # 同时保留官方 DERP 作为备用(删除这行则纯私有)
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  auto_update_enabled: true
  update_frequency: 24h

database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# ACL 策略模式:database 表示通过 Web UI / CLI 管理,存入 SQLite
# file 模式则从 policy.hujson 文件读取(只读,无法通过 Headplane 编辑)
policy:
  mode: database

# MagicDNS 配置(base_domain 必须与 server_url 不同)
dns:
  magic_dns: true
  base_domain: ts.k330.com
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 8.8.8.8

unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

# 禁止 Tailscale 节点上报日志到 Tailscale Inc.
logtail:
  enabled: false

taildrop:
  enabled: true
auto_update:
  enabled: false
EOF

初始 ACL 策略(CLI 导入)

使用 database 模式时,策略存储在 SQLite 里,通过 CLI 或 Headplane Web UI 管理。

先创建初始策略文件:

sudo tee /etc/headscale/policy.hujson > /dev/null << 'EOF'
{
  "acls": [
    { "action": "accept", "src": ["*"], "dst": ["*:*"] }
  ]
}
EOF
sudo chown -R headscale:headscale /etc/headscale

再导入到数据库:

sudo headscale policy set -f /etc/headscale/policy.hujson
# Policy updated.

# 查看当前策略
sudo headscale policy get

之后可直接在 Headplane Web UI(https://hp.k330.com/admin)里可视化编辑 ACL,无需操作文件。


四、配置 Caddy 反向代理

在现有 Caddyfile 末尾追加:

hs.k330.com {
    # 反代到 Headscale(含 DERP WebSocket 流量)
    reverse_proxy localhost:8085 {
        flush_interval -1
        transport http {
            read_timeout 300s
            write_timeout 300s
        }
    }
}

然后重载 Caddy:

sudo systemctl reload caddy
# 或
sudo caddy reload --config /home/sdmike/Caddyfile

为什么 DERP 不需要单独的子域? 内置 DERP 通过 /derp 路径挂载在 hs.k330.com 上,Caddy 的 flush_interval -1 确保 WebSocket 长连接不被截断。


五、创建 systemd 服务

sudo tee /etc/systemd/system/headscale.service > /dev/null << 'EOF'
[Unit]
Description=Headscale VPN Control Plane
Documentation=https://headscale.net
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=headscale
Group=headscale
RuntimeDirectory=headscale
RuntimeDirectoryMode=0750
ExecStart=/usr/local/bin/headscale serve
Restart=on-failure
RestartSec=5
# 限制资源占用
LimitNOFILE=65536
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_RAW

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now headscale
sudo systemctl status headscale

查看日志确认启动正常:

journalctl -u headscale -f --no-pager
# 期望看到:headscale is running

六、防火墙放行端口

# UFW(Ubuntu/Debian)
sudo ufw allow 3478/udp comment "STUN for Headscale"
sudo ufw allow 41641/udp comment "WireGuard direct"  # 可选,提升直连成功率

# iptables(如果使用)
sudo iptables -A INPUT -p udp --dport 3478 -j ACCEPT
sudo iptables -A INPUT -p udp --dport 41641 -j ACCEPT

# Oracle Cloud / AWS 用户还需在控制台安全规则里放行以上 UDP 端口

七、首次配置:创建用户与注册设备

Headscale 通过"用户(user)"组织设备,类似 Tailscale 的账号。

# 创建用户
sudo headscale users create admin

# 查看用户列表
sudo headscale users list

# 生成预授权密钥(24小时有效,可重复使用)
# 注意:v0.29.x 的 --user 参数需传用户 ID(数字),不是用户名
sudo headscale preauthkeys create --user 1 --reusable --expiration 24h
# 输出类似:hskey-auth-xxxxxxxxxxxxxxxx

八、客户端接入

所有平台都使用官方 Tailscale 客户端,只是把控制服务器地址指向自建的 https://hs.k330.com

先在服务端生成预授权密钥(auth key),接入时用它代替网页登录:

sudo headscale preauthkeys create --user 1 --reusable --expiration 24h
# 输出:hskey-auth-xxxxxxxxxxxxxxxx(保存好,下面各平台都要用)

macOS

下载tailscale.com/download/mac(App Store 或直装包)

安装后打开 Tailscale,不要点"登录",改用命令行:

# 连接到自建 Headscale
sudo tailscale up \
  --login-server https://hs.k330.com \
  --auth-key hskey-auth-你的密钥 \
  --accept-dns=true \
  --accept-routes=true

# 验证
tailscale status
tailscale ip -4     # 输出 VPN IP,如 100.64.0.2

Windows

下载tailscale.com/download/windows

安装后不要用界面登录,打开 PowerShell(管理员)执行:

tailscale up `
  --login-server https://hs.k330.com `
  --auth-key hskey-auth-你的密钥 `
  --accept-dns=true `
  --accept-routes=true

# 验证
tailscale status
tailscale ip -4

如果提示找不到 tailscale 命令,路径通常在 C:\Program Files\Tailscale\tailscale.exe,加到 PATH 或用完整路径执行。


Linux

# 一行安装(支持 Debian / Ubuntu / Fedora / Arch 等)
curl -fsSL https://tailscale.com/install.sh | sh

# 连接
sudo tailscale up \
  --login-server https://hs.k330.com \
  --auth-key hskey-auth-你的密钥 \
  --accept-dns=true \
  --accept-routes=true

# 验证
tailscale status

iOS

下载:App Store 搜索 Tailscale,或直达链接:apps.apple.com/app/tailscale/id1470499037

  1. 打开 App → 右上角头像 → Log in with custom server
  2. 输入控制服务器地址:https://hs.k330.com,点 Connect
  3. App 会显示一个 Node Key(格式如 nodekey:xxxxxx
  4. 在服务端注册该设备:
    sudo headscale nodes register --user 1 --key nodekey:xxxxxx
  5. 回到 App,设备即接入 VPN

如果你用的是预授权密钥,步骤 3-4 可以跳过——App 里直接填 auth key 即可(位置:Log in with custom server → 展开 Advanced 选项 → Auth key)。


Android

下载:Google Play 搜索 Tailscale,或直达:play.google.com/store/apps/details?id=com.tailscale.ipn

无 Google Play 的设备可下载 APK:pkgs.tailscale.com/stable/#android

操作步骤与 iOS 相同:登录时选择自定义服务器 → 填入 https://hs.k330.com


九、验证连接

# 查看所有已注册设备
sudo headscale nodes list

# 在任意两台设备间测试延迟
ping 100.64.0.2

# 查看当前连接路径(直连 or 中继)
tailscale ping 100.64.0.2
# 输出含 "via DERP" 表示走中继,无此字样表示直连

# 查看 DERP 地图(确认自建节点在列)
tailscale netcheck

十、日常管理速查

# 撤销设备
sudo headscale nodes delete --identifier <节点ID>

# 轮转预授权密钥(旧密钥失效)
sudo headscale preauthkeys expire --key <key>

# 查看路由(子网路由器功能)
sudo headscale routes list

# 启用某设备的子网路由(让整个内网可达)
sudo headscale routes enable --route <routeID>

# 重启服务
sudo systemctl restart headscale

# 备份数据库(所有设备注册信息在此)
sudo cp /var/lib/headscale/db.sqlite ~/headscale-backup-$(date +%Y%m%d).sqlite

进阶:启用 Exit Node(全局 VPN 出口)

将某台设备设为出口,让其他设备的所有流量从该机器出去:

# 在要做出口的设备上
sudo tailscale up --advertise-exit-node

# Headscale 服务端审批路由
sudo headscale routes list
sudo headscale routes enable --route <exit-route-id>

# 在其他设备上使用该出口
sudo tailscale up --exit-node=100.64.0.X

进阶:ACL 访问控制与用户隔离

默认策略允许所有设备互通,生产环境建议按角色划分用户并收紧权限。

用户规划

Headscale 通过"用户(user)"组织设备,先按角色建好用户,再给每个用户发专属 auth key:

sudo headscale users create share   # 共享设备(NAS、服务器等,对全员可见)
sudo headscale users create user    # 普通用户(只能访问自己的设备)
sudo headscale users create guest   # 访客(只能看到 guest 组内的其他访客设备)

sudo headscale users list
# 输出:1=admin  2=share  3=user  4=guest

# 按用户 ID 生成专属 auth key
sudo headscale preauthkeys create --user 2 --reusable --expiration 24h  # share
sudo headscale preauthkeys create --user 3 --reusable --expiration 24h  # user
sudo headscale preauthkeys create --user 4 --reusable --expiration 24h  # guest

ACL 策略

注意:Headscale v0.29.x 的 policy 格式中,用户名必须带 @ 后缀(如 share@)。

sudo headscale policy set -f - << 'EOF'
{
  "groups": {
    "group:shared": ["share@"],
    "group:users":  ["user@"],
    "group:guests": ["guest@"]
  },
  "acls": [
    // share 组的设备:所有人都能访问
    { "action": "accept", "src": ["autogroup:member"], "dst": ["group:shared:*"] },
    // 每个用户只能访问自己的设备
    { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"] },
    // guest 组内部互通(访客之间可以互相访问)
    { "action": "accept", "src": ["group:guests"], "dst": ["group:guests:*"] }
  ]
}
EOF

效果汇总:

flowchart LR A[admin 设备] -->|只有 admin 自己| A B[share 设备\nNAS/服务器] -->|所有人可访问| B C[user 设备] -->|只有 user 自己| C D[guest A 设备] <-->|guest 之间互通| E[guest B 设备] X[admin] -->|可访问| B Y[user@] -->|可访问| B Z[guest@] -->|可访问| B
用户 能访问 share 能访问 admin 能访问其他 user 能访问同组 guest
admin ✓(自己)
share
user
guest

查看与更新策略

# 查看当前策略
sudo headscale policy get

# 通过文件更新
sudo headscale policy set -f /etc/headscale/policy.hujson

使用 Headplane(https://hp.k330.com/admin)可在 Web 界面直接编辑 ACL,无需命令行。


进阶:安装 Headplane Web UI

Headscale 本身没有 Web 界面,Headplane 是目前功能最完整的第三方管理 UI,支持设备管理、ACL 编辑、DNS 配置等。

前置:为 Caddy 添加 hp.k330.com

hp.k330.com {
    reverse_proxy localhost:3001 {
        flush_interval -1
        transport http {
            read_timeout 120s
            write_timeout 120s
        }
    }
}

构建与安装

Headplane 需要 Go ≥ 1.25 和 Node.js 22 + pnpm:

# 安装 pnpm(如果没有)
npm install -g pnpm@10

# 克隆并构建
git clone --depth=1 --branch v0.6.3 https://github.com/tale/headplane.git /home/你的用户名/headplane
cd /home/你的用户名/headplane
pnpm install
pnpm build

生成 Headscale API Key

sudo headscale apikeys create --expiration 365d
# 输出:hskey-api-xxxxxxxxxxxx

配置文件 /etc/headplane/config.yaml

sudo mkdir -p /etc/headplane
sudo tee /etc/headplane/config.yaml > /dev/null << 'EOF'
server:
  host: "127.0.0.1"
  port: 3001
  base_url: "https://hp.k330.com"
  cookie_secret: "填入32位随机字符串"   # openssl rand -hex 16
  cookie_secure: true
  data_path: "/var/lib/headplane"

headscale:
  url: "http://127.0.0.1:8085"
  config_path: "/etc/headscale/config.yaml"
  api_key: "hskey-api-你的密钥"
EOF
sudo mkdir -p /var/lib/headplane

让 Headplane 运行用户能写 Headscale 配置:

sudo usermod -aG headscale 你的用户名
sudo chmod g+rw /etc/headscale/config.yaml /etc/headscale/policy.hujson

systemd 服务

sudo tee /etc/systemd/system/headplane.service > /dev/null << 'EOF'
[Unit]
Description=Headplane UI for Headscale
After=headscale.service
Requires=headscale.service

[Service]
Type=simple
User=你的用户名
WorkingDirectory=/home/你的用户名/headplane
ExecStart=/bin/bash -c 'source ~/.nvm/nvm.sh && node /home/你的用户名/headplane/build/server/index.js'
Restart=on-failure
RestartSec=5
Environment=HEADPLANE_CONFIG_PATH=/etc/headplane/config.yaml

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now headplane

访问 https://hp.k330.com/admin,用生成的 API Key 登录。


实操清单

  • DNS 添加 hs.k330.com A 记录,指向服务器公网 IP
  • (可选)DNS 添加 derp.k330.com A 记录,备用
  • (可选)DNS 添加 hp.k330.com A 记录,用于 Headplane UI
  • Cloudflare 用户:关闭以上子域的橙色云(改为 DNS only)
  • 下载 Headscale 二进制到 /usr/local/bin/headscale
  • 创建 headscale 系统用户及目录
  • 写入 /etc/headscale/config.yamlpolicy.mode: database
  • Caddyfile 追加 hs.k330.comhp.k330.com 块,重载 Caddy
  • 写入 /etc/systemd/system/headscale.service,启用并启动
  • 防火墙放行 UDP 3478 和 UDP 41641
  • headscale users create admin 创建用户
  • headscale preauthkeys create --user 1 生成预授权密钥
  • headscale policy set -f policy.hujson 写入 ACL(按角色隔离:share 全局可见,user 自见,guest 组内互通)
  • 构建并启动 Headplane,访问 https://hp.k330.com/admin
  • 服务器本机接入(VPN IP: 100.64.0.1,主机名: server-k330
  • 其他设备接入:手机 / 电脑运行 tailscale up --login-server https://hs.k330.com --auth-key <key>
  • tailscale ping 验证设备间连通性
  • tailscale netcheck 确认自建 DERP 节点已被识别
  • 备份 /var/lib/headscale/db.sqlite