NordBastion 的北极熊吉祥物站在一间幽暗的北欧石厅中,一只爪子搭在一根刻满符文的拱顶石方尖碑上,头顶悬浮着一圈青蓝色的全息密钥光环,五个发光的设备图标——笔记本电脑、手机、平板、服务器机塔与地球——以明亮的青蓝色网状连线彼此直接相连,悬在半空,光环垂下的虚线密钥光丝分别落向每一台设备,背景是一个服务器机架和一扇被极光照亮的拱形窗户
操作指南 · 私有组网·阅读 19 分钟 · 动手实践 50 分钟

你自己的控制服务器。
值得握在手里的,是那份地图。

Mesh VPN 需要有人来分发密钥。用 Tailscale,这个人是一家公司;用纯 WireGuard,这个人永远是你自己,靠手工维护。Headscale 是第三种答案:同一个开源客户端,对话对象换成了一台你匿名租来的协调服务器。六个步骤,打造一个设备地图、访问规则和兜底中继都完全属于你的 tailnet——把 Headscale 部署在 Caddy 之后、把 ACL 白纸黑字写下来、把 MagicDNS 指向你自己的解析器、配置子网路由器、出口节点和内嵌 DERP。这台机器每月只需 $3.90。已在 Debian 12 上测试通过。

六个步骤
  1. 01

    选择

    WireGuard, Tailscale, Headscale

  2. 02

    配置

    启动前先定好的三个名字

  3. 03

    部署

    Headscale、Caddy,一份配置

  4. 04

    注册

    用户、预授权密钥、客户端

  5. 05

    路由

    子网、出口节点、ACL

  6. 06

    Keep

    DERP、备份、升级

开始之前 · 控制平面是什么

没有人能读取流量。但仍然有人握着地图。

WireGuard 是刻意留白的。它是两把公钥之间一条小巧、快速、可审计的隧道,对这两把密钥要如何找到彼此,它完全不发表任何意见。正是这种留白,才成就了这个协议今天的优秀——但也正因如此,一个十五台设备的 WireGuard 网络,最终会变成一张表格。每个对等节点都需要知道其余所有对等节点的公钥和当前地址,而每一台在办公室、火车和酒店之间辗转的笔记本电脑,都会让这张表格的一部分失效。

Mesh VPN 正是针对这个具体问题的答案,它的结构值得说清楚,因为这决定了你到底在信任什么。数据平面完全不变:每台设备在本地生成自己的密钥对,私钥留在本地,直接与对等设备加密通信。在这之上是一层控制平面——一个收集公钥和端点信息的小型服务,它告诉每台设备可以与哪些对等设备通信,并帮助两台身处恶劣路由环境的设备互相找到对方。这项服务从不持有私钥,也从不会看到一个字节的明文。

它真正握有的是那份地图,而这份地图比大多数人以为的要有意思得多。每一个设备名,每一个用户名。每一把公钥。每台设备此刻正在使用的公网地址,这是你笔记本电脑和手机实际所在位置的实时记录。每台设备连接和断开的时间,勾勒出你一天的作息。还有那份决定谁能访问什么的策略,勾勒出你的组织架构。这一切都不需要任何人心怀恶意才会发生——它本来就是协调服务器存在的意义。

还有一个特性,比元数据更值得关注。控制平面是负责分发密钥的角色,所以它同样也能够添加一把密钥。一台被攻破、或被强制配合的控制服务器,可以告诉你的设备说有一台新机器是网络的合法成员——而你的设备只是在做它们本该做的事,就会开始向它加密通信。Tailscale 对此足够重视,专门做了一套签名功能来防范这一点。自托管则是从另一个方向解决同一个问题:能做这件事的人,变成了你自己。下面是四种安排方式,以及每一种实际上会泄露出去什么:

属性 手动配置的 WireGuard Tailscale,托管版 在你的 VPS 上运行 Headscale Headscale + 你自己的 DERP
谁能读取载荷除了对等设备之外,谁也看不到除了对等设备之外,谁也看不到除了对等设备之外,谁也看不到除了对等设备之外,谁也看不到
谁握有设备地图你自己,写在一份文本文件里一家公司,在它自己的司法辖区里你自己,在你的 VPS 上你自己,在你的 VPS 上
谁能看到设备的连接来源只有对等设备自己协调服务器,持续在线你自己的服务器,持续在线你自己的服务器,持续在线
直连失败时由谁来中继没有人中继——连接直接失败服务商的中继网络默认情况下,依然是服务商的中继你自己的中继,在你自己的数据中心里
添加第二十台设备十九次配置修改一条命令一条命令一条命令
适合选择它的情形设备不多,且都不移动你不想做任何运维,也接受地图不在自己手里你想要那份地图,而且能运行一台小机器你既想要那份地图,也想要那条兜底路径

第一栏可不是拿来凑数的靶子。对于少数几台固定不变的服务器来说,手写 WireGuard 配置确实是更好的工程答案,还能少维护一项服务的补丁。本指南要讲的,正是那张表格开始变得不再好笑的临界点。

步骤 01 · 选择

你需要控制平面吗?数一数会移动的设备有几台。

面对「我想让我的机器之间私密互访」这个诉求,有三个诚实的答案,本指南只是其中一个。纯 WireGuard 是最小的一种:一个内核模块、每个对等节点一份配置文件、除了接口本身没有别的守护进程,除了操作系统之外没什么需要保持更新。如果你的设备都是地址固定的服务器,而且一只手就数得过来,那就不必往下看了,去读WireGuard 指南就够了。你最终会得到更少的软件,以及一份更短的「可能出问题的清单」。

Tailscale 是三者中规模最大的:底层同样是 WireGuard,再加上一套协调服务、几乎能穿透任何环境的 NAT 穿透能力、遍布多个大洲的中继网络、网页控制台、单点登录和支持合同。它确实是优秀的软件,免费套餐足以轻松满足一个家庭的需求。代价不是金钱——而是你名下每台设备的地图,以及每台设备每次连接来源的记录,都存放在一家公司的数据库里。

Headscale 是第三种答案,也是三者中最克制的一种:它是协调服务器的开源重新实现,由你自己运行,与同一个官方客户端对话。你保留了自动化和 NAT 穿透能力;换来的是一个需要自己运维的小型服务、一套不完整的功能,以及需要认真阅读说明的升级。没人应该假装这是免费的。它换来的只有一件事,却正是这个网站在乎的那件事:地图不在别人手里。

五台设备的门槛

手动配置的 WireGuard,其工作量会随设备数的平方增长:四个对等节点是六条连接,十个是四十五条,二十个就是一百九十条。再加上一台漫游笔记本电脑——它的端点会随着加入的每个网络而改变——维护工作就不再是偶尔为之的事了。实践中,临界点大约在五台设备左右,或者第一台每天变换网络的设备出现时,又或者第一次出现一条无法用「所有人都能访问所有东西」来表达的访问规则时。

还有一件事最好现在就决定,而不要拖到以后,因为它会影响你安装的内容:这个网络是只给你自己用,还是会有别人加入。只有一个人使用的 tailnet,内部完全开放也不会出什么问题。而只要有同事、承包商或家人加入,从第一周起就需要写明白的访问规则——也就是步骤 05——而且需要每个人都有独立的用户,而不是所有人共用一个登录账号。提前决定好,只要花一段文字的功夫;事后再补救,则要搭上一个下午。

步骤 02 · 配置

三个名字,要在首次启动前就决定好。这三个都很难事后更改。

先把机器下单买好。在控制面板里:下单 → VPS → Sentinel,镜像选 Debian 12,机房选你想让这份地图落脚的地方——赫尔辛基、斯德哥尔摩、奥斯陆或雷克雅未克,任选其一。开户不需要提供邮箱地址,账单可用门罗币、比特币、闪电网络或其他支持的资产结算。如果你还打算按步骤 06 运行中继,就选一个离你大多数设备最近的机房;到中继的延迟,就是它承载的每一条连接的延迟。

然后跑一遍首小时加固清单——把 SSH 换到非默认端口并只允许密钥登录、开启无人值守升级、配置默认拒绝的防火墙。以下所有内容都假定这台机器还没有落入别人之手,而且这台机器比大多数机器都更值得重视:它是决定你名下每台设备该信任哪些密钥的那台机器。

在任何设备注册之前,先确定这三件事

每台设备一加入,就会把这三样东西烙进自己的状态里,之后再想改动其中一样,就意味着要手工把整支设备群重新注册一遍。第一:控制服务器 URL——每个客户端会拨打的确切公开 HTTPS 地址,包括协议和主机名在内。第二:tailnet 的 DNS 后缀,也就是 MagicDNS 在内部应答的域名;它必须是你自己掌控的域名,而且不能是控制服务器自身主机名的父级或子级——这是首次启动时最常见的失败原因。第三:tailnet 分配的地址范围,通常 IPv4 用 100.64.0.0/10 这个运营商级 NAT 网段,IPv6 用一个唯一本地 /48 网段;除非它们已经和你正在用的某个网络冲突,否则保留默认值即可。

举一个可以直接照搬的例子:控制服务器用 vpn.example.com 应答,tailnet 后缀是 ts.example.com,地址范围保持默认值。请注意,这个后缀是控制服务器主机名的「兄弟」,而不是它的「父级」——vpn.example.com 和 ts.example.com 共享同一个可注册域名,但彼此互不包含。把一条 A 记录——如果想通过 IPv6 访问,再加一条 AAAA 记录——指向这台机器,并在动手之前等它完成传播,因为反向代理会在首次启动时就尝试为这个名字申请证书。

然后趁着还没有任何服务在监听的时候,把防火墙规则写好。控制服务器出于必要必须是一个公开的 HTTPS 端点:无论哪个网络里的哪台设备,都得能连上它。UDP 端口只有在你打算按步骤 06 运行自己的中继时才需要,现在就开放它,比日后排查它为什么没开放要省事得多。

ufw default deny incoming
ufw allow 47222/tcp                  # administrative SSH, moved
ufw allow 80/tcp                     # ACME HTTP-01 challenge
ufw allow 443/tcp                    # the control server, and DERP over HTTPS
ufw allow 3478/udp                   # STUN, only if you run your own relay
ufw enable
ufw status numbered

从官方仓库安装 Docker,而不是用发行版自带的软件包,这样才能确保 Compose v2 存在且是最新版本。然后创建两个目录,用来存放所有值得备份的内容——一个放配置,一个放状态——并克制住往里面塞其他东西的冲动。

curl -fsSL https://get.docker.com | sh
docker compose version

install -d -m 0750 /srv/headscale/config
install -d -m 0750 /srv/headscale/data
cd /srv/headscale
步骤 03 · 部署

两个容器,一份文件。始终锁定版本。

只有 Headscale 和 Caddy,再无其他。Headscale 在 Docker 网络内部监听普通 HTTP;Caddy 负责终结 TLS,首次启动时申请证书,并在无需提醒的情况下自动续期。指标监听端口只对 localhost 开放,因为那是内部调试接口,不是对外的公开接口。

在写这个文件之前,先定一条规矩:把镜像锁定到确切的版本号。Headscale 是尚未到 1.0 版本的软件,配置项和 CLI 子命令在历次次要版本里都不止一次改过名字。用浮动标签,会让一次再普通不过的容器重启,变成一次事故——而这项服务的职责,恰恰是让你能连上其他一切东西。

/srv/headscale/docker-compose.yml

services:
  headscale:
    image: docker.io/headscale/headscale:0.26            # pin it; never :latest
    container_name: headscale
    command: serve
    volumes:
      - ./config:/etc/headscale
      - ./data:/var/lib/headscale
    ports:
      - "3478:3478/udp"                                  # STUN, for your own DERP
    restart: unless-stopped

  caddy:
    image: caddy:2-alpine
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    restart: unless-stopped

volumes:
  caddy_data:

/srv/headscale/Caddyfile

vpn.example.com {
    reverse_proxy headscale:8080
}

这就是完整的代理配置了。Caddy 支持 HTTP/2,也会自动升级长连接而无需特别告知,这一点在这里很重要,因为每个客户端都会保持一条长连接来接收地图更新。拿你锁定版本时自带的示例配置文件来改,而不要从零开始写——这个文件很长,大部分都是合理的默认值,真正要关心的配置项是这几个:

/srv/headscale/config/config.yaml — the lines that matter

server_url: https://vpn.example.com          # exactly what clients will dial
listen_addr: 0.0.0.0:8080                    # inside the Docker network only
metrics_listen_addr: 127.0.0.1:9090          # debug surface, keep it local

prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

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

database:
  type: sqlite                               # postgres for a large fleet
  sqlite:
    path: /var/lib/headscale/db.sqlite

dns:
  magic_dns: true
  base_domain: ts.example.com                # NOT a parent or child of server_url
  nameservers:
    global:
      - 1.1.1.1                              # replaced by your own in step 05

启动它,然后去看日志,而不是靠猜。如果首次启动时无法获取证书,或者因为 DNS 后缀与服务器主机名重叠而被拒绝,日志会说得清清楚楚,然后停下来。

docker compose up -d
docker compose logs -f headscale

# From your laptop — a control server that is up answers this:
curl -s https://vpn.example.com/health

首次启动会在 /srv/headscale/data 中生成私钥材料。现在就把它复制到安全的地方——趁着还没有任何一台设备注册,等第一批设备注册完之后再复制一次:一旦丢失这把密钥,所有设备都会拒绝它们一直信任的服务器,唯一的补救办法就是手工把所有设备重新注册一遍。步骤 06 会把这次复制变成一份真正的备份。

步骤 04 · 注册

一个用户,一把密钥,一个客户端。然后检查它是否走了直连。

headscale 命令住在容器内部,所以每个管理操作都要通过 docker exec 来执行。先创建一个用户——用户是设备的持有者,也是访问规则所引用的基本单位,所以给它起一个人名或角色名,而不是机器名。

hs() { docker exec -it headscale headscale "$@"; }

hs users create alice
hs users list                      # note the numeric id — newer releases want it

# A single-use key that expires in an hour is the right default.
hs preauthkeys create --user 1 --expiration 1h

然后在设备上启动官方客户端,把它指向你自己的服务器,而不是默认服务器。这是一个官方支持的参数,不是什么歪招——客户端是开源的,登录服务器本来就是一个可配置项。

# On the device (Debian/Ubuntu shown; every platform has an equivalent):
curl -fsSL https://tailscale.com/install.sh | sh

sudo tailscale up \
  --login-server https://vpn.example.com \
  --auth-key <the-key-you-just-created>

tailscale status
tailscale ip -4

如果不用预授权密钥,客户端就会打印出一个 URL;打开它会看到一条要在服务器上运行的注册命令,这是交互式的路径,适合那种你正坐在它面前的设备。不管走哪条路,都先把第二台设备也搞定再庆祝——只有一台设备的网络什么也证明不了。用它的 tailnet 地址 ping 一下,然后仔细看状态输出:

hs nodes list

# On a client, the word after the peer name is the one that matters:
tailscale status
#   100.64.0.2  laptop  alice  linux  active; direct 203.0.113.9:41641
#                                              ^^^^^^ direct, not relay

tailscale netcheck                 # why it chose what it chose

direct(直连)是指两台设备互相找到了对方,流量点对点传输。relay(中继)是指流量正经过一台 DERP 服务器——这样也能正常工作,但会增加延迟,并在路径上多出第三台机器,而默认情况下这台机器不属于你。步骤 06 要解决的,正是决定它究竟是哪一种。

手机、平板与苹果的那道弯路

桌面平台用一个命令行参数就能搞定。Android 在应用内暴露了自定义服务器选项,藏在设置界面的版本号后面。苹果平台没有应用内输入框:你需要安装一个小型配置描述文件,在登录前设好控制服务器 URL,这一步麻烦一次,之后就一直有效。两件事能帮你省时间。去查 Headscale 文档里针对各客户端的说明页,而不是靠猜,因为苹果这套流程已经改过不止一次。还有,先把应用从 Tailscale 自家服务登出——一个已经在那边持有会话的应用,往往会拒绝切换到别的服务器,除非先登出,有时甚至要重装。

有两个习惯值得立刻养成。给人用的密钥,用一次性、短期有效的预授权密钥;可重复使用或临时性的密钥,只留给真正需要它们的自动化场景——临时密钥注册的设备,一旦离线就会自动从网络中移除,这对 CI 运行器来说再合适不过,对笔记本电脑来说则恰恰相反。同时现在就想清楚你对密钥过期的立场:每台注册设备都带着一个过期时间,密钥一旦过期,设备就会失联,直到有人重新完成身份验证。这在笔记本电脑上是个安全特性,在凌晨三点的无头服务器上却是个故障制造机。去查一下你这个版本是怎么管理过期时间的——相关子命令挪过位置——然后针对每台设备有意识地去设置它,而不要等踩了默认值的坑才发现问题。

步骤 05 · 路由

广播了不等于批准了。两条命令,两台机器。

一个所有设备都能互相访问的 tailnet 已经很有用了。再加三样东西,它才会从一堆隧道变成真正的网络,而这三样东西都有一个容易让人栽跟头的共同点:客户端负责广播能力,服务器必须批准它。一条只广播、未获批准的路由,就等于根本不存在,而且不会有任何提示——症状就是你能 ping 通路由器本身,却连不上它后面的任何设备。

子网路由器——把整个局域网接入 tailnet。 家庭或办公网络里的一台机器广播该网络的地址范围;其余所有设备就能访问它背后的打印机、NAS 和管理界面,而不需要在那些设备上另外安装任何东西。记得先在内核里开启转发功能,否则数据包会在路由器处悄无声息地止步,不会有任何错误提示。

# On the machine that sits inside the LAN:
echo 'net.ipv4.ip_forward = 1'          | sudo tee /etc/sysctl.d/99-ts.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-ts.conf
sudo sysctl -p /etc/sysctl.d/99-ts.conf

sudo tailscale up --login-server https://vpn.example.com \
  --advertise-routes=192.168.1.0/24 --accept-routes

# On the control server — approve it, or it does not exist.
# The subcommand moved between releases; check `hs nodes --help` first.
hs nodes list
hs nodes approve-routes --identifier 3 --routes 192.168.1.0/24

出口节点——把所有流量都经由 VPS 转发出去。 同一套机制,只是路由范围更大:一台把自己广播为出口节点的设备,愿意承载另一台设备的整条互联网连接。在不受信任的咖啡馆或酒店网络里,这正是你想要的,也是本指南里最接近「个人 VPN」的东西。

# On the VPS that will do the exiting (forwarding must be on, as above):
sudo tailscale up --login-server https://vpn.example.com --advertise-exit-node

# Approve it on the control server, then on the laptop:
tailscale set --exit-node=100.64.0.1 --exit-node-allow-lan-access
tailscale set --exit-node=                    # turn it off again

出口节点不是商业 VPN,也别把它当成商业 VPN 来说服自己。它的地址只属于你一个人,而且是固定的——这一点在对付咖啡馆 Wi-Fi 时非常好用,在对抗跨站点的长期关联分析时却毫无用处,这个取舍在旗舰指南里说得很清楚。另外提醒一句:你完全可以把出口节点放在另一台更便宜的 VPS 上,让控制服务器彻底不承担任何转发流量。

访问规则——在第二个人加入之前先写好。 默认策略是「全部放行」,对只有一个人的 tailnet 来说这没问题,但只要多一个人加入就错了。策略是一份小小的 HuJSON 文档:用户分组、机器标签,以及一份规则列表,写明哪些来源可以在哪些端口上访问哪些目标。把配置指向这个文件,用版本控制管理它,每次修改后重新加载。

{
  "groups": {
    "group:admin":     ["alice"],
    "group:contractor":["bob"]
  },
  "tagOwners": {
    "tag:server": ["group:admin"],
    "tag:exit":   ["group:admin"]
  },
  "acls": [
    // Admins reach everything.
    { "action": "accept", "src": ["group:admin"], "dst": ["*:*"] },

    // The contractor reaches one machine, on two ports, and nothing else.
    { "action": "accept", "src": ["group:contractor"],
      "dst": ["tag:server:22,443"] }
  ],
  "autoApprovers": {
    "routes":   { "192.168.1.0/24": ["tag:server"] },
    "exitNode": ["tag:exit"]
  }
}

关于这个文件,有两点要说明。策略语言是 Tailscale 定义的,Headscale 实现的是它的一个庞大且不断扩充的子集,而不是全部——在依赖某条冷门规则之前,先查一下你这个版本的文档。另外,策略文件存放的位置也变过:较旧的版本在顶层配置里直接指定一个路径,较新的版本则把它归到一个策略小节下,还多了一种可以把规则直接存进数据库的模式。两种方式都有文档说明;真正要紧的是你锁定的那个版本采用的是哪一种。

MagicDNS——短名称,以及无处不在的自有解析器。 开启 MagicDNS 后,每台设备都会响应步骤 02 中所选后缀下的一个短名称,所以 ssh nas 在任何地方都能直接用,不需要 hosts 文件。更有意思的是后半部分:控制服务器还会把一份域名服务器列表推送给每个客户端,这意味着你在私有 DNS 解析器指南中搭建的解析器,可以在移动数据网络下为你的手机提供服务——过滤、缓存,且不记录任何日志——同时它监听的地址还是一个完全无法从公网访问的 tailnet 地址。

dns:
  magic_dns: true
  base_domain: ts.example.com
  override_local_dns: true          # beat whatever DHCP handed the device
  nameservers:
    global:
      - 100.64.0.5                  # your resolver, on the tailnet
步骤 06 · 留存

你自己的中继,你自己的备份。升级前先读说明,再动手。

那个你早已忘记自己还在用的中继。 当两台设备无法建立直连时——两端都是对称型 NAT、防火墙吞掉了 UDP、运营商搞了些花样——流量就会退回到 DERP 中继。它依然是端到端加密的,所以中继承载的是密文。中继能看到的是谁在跟谁通信、通信量多少、什么时候通信。开箱即用的情况下,Headscale 会把公共中继地图交给你的客户端,而这些中继正是由你想要摆脱的那家公司运营的。Headscale 自带一个内嵌中继;启用它,就能把这条兜底路径放进和这一整套系统相同的北欧数据中心里。

derp:
  server:
    enabled: true
    region_id: 999                  # custom regions live in the 900s
    region_code: "nb-hel"
    region_name: "Helsinki (self-hosted)"
    stun_listen_addr: "0.0.0.0:3478"
  urls:
    - https://controlplane.tailscale.com/derpmap/default   # remove for yours only
  auto_update_enabled: true
  update_frequency: 24h

把公共中继地图和你自己所在地区一并保留,是务实的默认做法:客户端会优先选用最近的中继,所以同一半球的设备会用上你的中继,而地球另一端的设备也不会被困在单一的北欧跳点后面。移除公共 URL 能让 tailnet 变得彻底自成一体,代价是远端设备的兜底效果会变差。两种做法都说得过去;关键是要有意识地选择,而不是稀里糊涂地默认。

四个文件,异地存放,全部加密。 这份备份小到如果弄丢了都会觉得难堪:数据库、私钥材料、配置文件和策略文件。加在一起,在任何存储账单上都只是舍入误差,可一旦没了密钥材料,网络里的每台设备都会拒绝这台服务器。把它们发送到不是这台机器的地方——最好是另一个司法辖区,这正是备份指南要讲的内容——然后特意在一台备用 VPS 上完整还原一次,趁着还用不上的时候就先试一遍。

# Stop briefly for a consistent SQLite copy, or use .backup on a live db.
docker compose stop headscale
tar czf /tmp/headscale-$(date +%F).tar.gz -C /srv/headscale config data
docker compose start headscale

restic -r sftp:backup@backup.example.com:/srv/restic backup \
       /tmp/headscale-$(date +%F).tar.gz

升级,才是真正的风险所在。 这是一款尚未到 1.0 版本的软件,行事作风也确实如此:配置项改过名、CLI 子命令挪过位置,甚至不止一次有某个版本需要按文档做迁移,而不是简单重启就行。这些都不是回避它的理由——而是应该养成一套固定流程的理由。拉取新镜像之前,先读发行说明。先做上面说的那次归档备份。把你的配置文件和新版本自带的示例文件做一次对比,因为改名的配置项正是在这里露出马脚的。一次只升一个次要版本,不要一口气跨三个版本。把旧的镜像标签记下来,因为一次三十秒就能完成的回滚,才是让整件事风险可控的关键。

然后设一个每月提醒,检查两件事:已注册设备的列表,好让六个月前就淘汰掉的机器不再是网络成员;以及到期时间,避免有设备在最糟的时刻悄无声息地掉线。Mesh VPN 是少数会「悄悄失败」的服务之一——一切看起来都正常运行,直到你恰好需要那台早已悄悄过期的设备时才发现问题。

诚实的部分 · 它做不到什么

你把地图夺了回来。但你并没有因此变得隐形。

自建控制平面是一项真实而具体的改善,而不是一场变戏法。就算以上这些全部做到位,仍有五件事不会改变,弄清楚它们,才是「真正的防线」和「自我安慰的说辞」之间的差别。

控制服务器是一个公开的、有名字的端点。 每台设备都要拨打它,所以它必须有一个主机名,而这个主机名一旦签发证书,就会立刻被记录进证书透明度日志。哪怕你从未在任何地方链接过它,它依然是可以被发现的,而它的地址正是你所有设备唯一共有的常量。用一个你已经公开使用的域名下的子域名,而不要用一个能看出用途的名字。

Headscale 是一个重新实现的项目,而且它自己也这么说。 它不是 Tailscale 构建或支持的产品,从设计上就注定功能不会完全对等,托管版产品所卖的一些东西——网页控制台、公共中继网络、打磨精致的单点登录流程——在这里要么不存在,要么只是一个没人审计过的第三方项目。在把某个功能纳入计划之前,先去查项目文档确认它是否存在,并且要把升级当成一项任务来对待,而不是放任它在后台自动发生。

默认的中继,依然是别人的。 在你启用内嵌 DERP 服务器之前,任何无法直连的连接都会退回到公共中继网络。载荷依然是端到端加密的,没有人能读取它——但「你的两台设备正在通信、通信量有多少」这件事本身,会经过一套不属于你的基础设施。这正是大多数人第一天下午最容易漏掉的那部分信任链。

出口节点是一个稳定的地址,而不是一群人。 把流量经由你自己的 VPS 转发,能骗过咖啡馆的网络和 ISP 的画像分析。但它挡不住关联分析,因为这个地址只属于你一个人,而且从不改变。商业 VPN 的共享地址在其他各方面都更差,唯独在这个特定威胁上更胜一筹。

机器依然放在一栋建筑物里。 租来的服务器运行在有门禁、有工作人员的数据中心的硬件上。改变的是:没有产品层面对你设备的索引、没有公司数据库记录每台设备的连接来源、也没有一份可能下个季度就被悄悄修改的隐私政策。不变的是:硬盘依然是实体的,而且不在你家里。

你实际买到的东西,精确地说是这样:你名下每一台机器的清单、每一台的连接来源记录,以及添加新设备的权限,如今全都保存在一台你以匿名方式租用的机器上。这是一个有限的主张,但也是一个真实的主张。它值不值这个价钱,取决于步骤 06 有没有做到位——一个没有经过实测还原的自建控制平面,算不上什么隐私上的改善,只是一个包装得更好看的单点故障。

选型 · 该订购什么

控制平面是免费的。你付钱买的是流量。

套餐 按月 资源 能轻松承载的内容
Sentinel$3.902 vCPU · 4 GB · 120 GB · 1 Gbps控制服务器、一个中继,再加轻度的出口节点使用。这是个正确的起点。
第二台 Sentinel$3.90出口节点,独立部署在自己的机器上让转发流量远离控制平面
Ravelin$16.908 vCPU · 16 GB · 480 GB · 2.5 Gbps供一个家庭日常使用的出口节点,满速线速运行
Bulwark$32.9016 vCPU · 32 GB · 960 GB · 5 Gbps公司的出口节点与繁忙的中继,二者合一

这张表里的选型都不是为「协调」这件事本身准备的。控制服务器每台设备只占用一条空闲连接,数据库大小以 KB 计;几百台设备也不会给最小的套餐带来任何压力。真正消耗资源的是转发流量——中继连接,更主要的是承载着某人整条互联网连接的出口节点。请按上行带宽和加密吞吐量来选型,并且记住每个套餐的带宽都不限量,唯一的问题只是你想要多快。

实战笔记 · 六个陷阱

六种出错的方式。其中两种看起来像是 DNS 问题。

陷阱 01 · 令人困惑

tailnet 后缀与服务器主机名重叠

如果选用的 MagicDNS 后缀是控制服务器主机名的父级或子级,会破坏名称解析,表现得就像一次 DNS 故障。让两者保持「兄弟」关系:服务器用 vpn.example.com,tailnet 用 ts.example.com。

陷阱 02 · 悄无声息

路由已广播,却从未被批准

客户端会一如既往地报告自己正在广播 192.168.1.0/24,但路由器背后没有任何设备响应,因为服务器从未批准过这条路由。哪里都不会有错误提示。先在服务器上批准,然后去查路由列表,而不是盯着客户端看。

陷阱 03 · 缓慢

一切都走中继,没有一次直连

一个封锁出站 UDP 的网络会迫使所有连接都走中继:能用,但更慢,而且路径上多了第三台机器。这时 tailscale status 会显示 relay 而不是 direct,netcheck 则会说明原因。要么修复防火墙,要么心知肚明地接受这一跳。

陷阱 04 · 暴露

默认的「全部放行」策略,外加一位加入 tailnet 的访客

一个全新的 tailnet 默认放行所有设备之间、所有端口的互访。对一个人来说这没问题,但只要有一台承包商的笔记本电脑加入,这就错了——包括那些他们根本不知道存在的管理界面。先写好策略,再发邀请。

陷阱 05 · 致命

从未备份过的私钥

重建机器、还原数据库,却忘了密钥材料——于是每台设备都会拒绝它昨天还信任的服务器。这时没有什么取巧的恢复办法,只有花一整天手工给手机重新注册。在第一台设备加入之前,先把数据目录复制好。

陷阱 06 · 本可避免

在一个尚未到 1.0 版本的服务上使用浮动镜像标签

配置项和 CLI 子命令在历次次要版本更新中都改过名字。如果用的是 :latest,一次毫不相关的重启就会拉取新版本,让你整个设备群的控制平面直接停摆。锁定标签,阅读发行说明,有意识地升级。

常见问题 · 自建 Mesh VPN

问题,已解答。

搭建过程中会遇到的十一个问题——从决定你到底要不要搭建它的那两个问题开始。

Headscale 是 Tailscale 官方产品吗?

不是,这一点值得说得精确一些。Headscale 是协调服务器——Tailscale 中负责分发公钥、端点和策略的那个部件——的一个独立开源重新实现。它不是由 Tailscale 构建、销售或支持的,没有商业支持合同,一旦出问题,你要做的是去看问题追踪器,而不是提工单。它真正做到的是讲同一套协议——官方开源 Tailscale 客户端本来就在讲这套协议,这也是客户端不用修改就能直接对接它的原因。由此带来两个实际后果。功能对等永远只会是部分的,因为这个项目追赶的是一个由规模大得多的团队维护、还在不断移动的目标——在围绕某个功能设计方案之前,先查一下项目自己的文档确认它是否存在。而升级也需要比一般的容器版本更新更多的谨慎:配置结构和好几个 CLI 子命令都在历次次要版本里改过名字,这正是人们把一套好端端的安装搞坏的最常见原因。

协调服务器能读取我的流量吗?

不能——这一点是这套架构里值得真正理解、而不是姑且相信的部分。每台设备都在本地生成自己的 WireGuard 密钥对,私钥的那一半永远不会离开设备。设备上传给控制服务器的,是它的公钥、当前端点信息和一些元数据;它下载到的,则是每个被允许访问的对等设备的同类信息。隧道随后在设备之间直接协商建立,加密方式就是普通的 WireGuard,用的是服务器从未持有过的两把密钥。所以控制服务器无法解密任何东西,Tailscale 的服务器同样如此。它能做到的是另一件不太被讨论的事:它掌握着地图。设备名称、用户名称、公钥、每台设备当前拨打的公网 IP、哪台设备在凌晨三点醒来联网,以及那份决定谁能访问谁的策略。它同时也是分发密钥的那个角色,所以一台被攻破或被强制配合的控制服务器,可以把一台设备加进你的网络。自托管解决不了数学问题;它只是把一个行政管理问题,搬到了一台你自己拥有的机器上。

如果控制服务器宕机,我的网络会怎样?

比大多数人想象的要小得多,这也是这套设计让人安心的地方。设备会缓存它们最后一次拿到的网络地图,而已经建立的 WireGuard 隧道是点对点的——它们不经过控制服务器转发,也不需要控制服务器才能继续工作。在正常工作日里重启一次 VPS,通常根本不会被察觉。真正停摆的是变化本身:新设备无法注册、换了网络的设备可能需要更久才能重新找到对等节点、策略修改无法生效,任何在服务器宕机期间密钥到期的节点也无法续期。所以理解控制服务器的正确方式,是把它当成控制平面而不是数据通路:它宕机只是个麻烦,不是断网。话虽如此,还是要把它当成生产环境来对待。做好备份(步骤 06),如果整个部门都依赖这个 tailnet,就把控制服务器放在自己独立的小机器上,不要和那些经常重启的东西共用。

Headscale 还是纯 WireGuard——控制平面到底什么时候才值得?

数一数你有多少台设备,再数一数其中有几台会移动。对于一个小而固定的设备集合,纯 WireGuard 是绝佳的答案:三四个对等节点、固定端点、每台设备一个配置文件,除了内核模块之外什么都不用运行。它比本指南里的任何方案都更少软件、更少攻击面、更少维护,我们的WireGuard 指南把它从头到尾都讲清楚了。代价在于 WireGuard 刻意不做密钥分发,所以每加一台新设备,就要在其余每台设备上都改一遍配置——这项工作量会随设备数的平方增长——而在酒店 Wi-Fi 和移动数据之间漫游的客户端,还需要有人不断地告诉所有人它们又搬到哪儿去了。控制平面买的正是这个省心。实践中的门槛大约在五台设备左右,或者出现第一台每天更换网络的笔记本电脑时,又或者你第一次想写一条「承包商的笔记本电脑只能访问预发布服务器,别的都不行」这样的规则时。在那之前,用 WireGuard,享受清静。

我手机上的 Tailscale 应用能配合 Headscale 使用吗?

可以——它就是同一个官方客户端,把它指向另一台控制服务器是官方支持的操作,不是什么破解手段。具体做法因平台而异,这也是最容易让人耗掉一整晚的地方。在 Linux、macOS 和 Windows 上,是客户端的一个命令行参数(--login-server)或对应的设置项。在 Android 上,这个选项就摆在应用里,藏在设置界面的版本号后面。在 iOS、iPadOS 和 tvOS 上,没有应用内的输入框:你需要安装一个小型配置描述文件,在登录前设好控制服务器 URL,第一次操作会有点麻烦,之后就一直有效。Headscale 文档专门为此保留了针对各平台的说明页,值得直接打开去看,而不要靠猜,因为苹果这套流程尤其改过不止一次。还有一条与平台无关的提醒:一个已经登录过 Tailscale 自家服务的应用,通常需要先登出——有时甚至要重装——才会接受切换到另一台服务器。

我需要自己运行 DERP 中继吗?

不是为了让网络能用——而是「基本私密」和「始终私密」之间的差别,所以要有意识地做决定。DERP 服务器是在两台设备无法建立直连时使用的中继:两端都是对称型 NAT、企业防火墙吞掉了 UDP、移动运营商搞了些花样,都会触发这种情况。中继流量依然是端到端加密的 WireGuard,所以中继运营方看到的是密文,不是内容。它能看到的是元数据:哪两台设备在通信、通信量多少、什么时候通信。开箱即用的情况下,Headscale 会把 Tailscale 的公共 DERP 地图交给你的设备,也就是说这些中继依然是 Tailscale 的——你自建了控制平面,却把兜底数据通路留给了你原本想摆脱的那家公司。Headscale 自带一个内嵌 DERP 服务器;启用它,就能让你在控制服务器所在的同一个北欧数据中心里拥有一个属于自己的中继。诚实地说,这里的取舍在于覆盖范围:Tailscale 在多个大洲运行中继,而你只运行一个,所以身处地球另一端的设备用它们的中继会更好。两者都用,把你自己的中继也纳入地图,是相当合理的折中方案。

我能让整个 tailnet 都使用我自己的 DNS 解析器吗?

可以,而且这是搭建这一整套系统最值得的理由之一。Headscale 能把 DNS 配置推送给每一台加入的设备,这样你在咖啡馆 Wi-Fi 下的笔记本电脑,和用移动数据的手机,都会经由你自己运行的解析器完成解析——带过滤、带缓存,而且本地网络看不到任何查询请求。把 tailnet 的全局域名服务器指向运行着你自己的 Unbound 或 AdGuard Home 的那台机器的 tailnet IP,再设置覆盖选项,让它压过 DHCP 分配给设备的任何设置。有两件事要做对。解析器必须只能在tailnet 内部访问,而不是暴露在公网上——这正是整件事的意义所在,也意味着你终于可以把 53 端口对外界彻底关闭。另外,MagicDNS 会给每台设备分配一个短名称,放在你选择的后缀之下,这个后缀必须是你自己掌控的,而且不能是控制服务器自身主机名的父级或子级;一旦弄错,就会出现一种看起来像 DNS 故障、实际上是配置错误的、令人困惑的失败。

VPS 需要多大?

单单跑控制平面:需求很小,而且比你想的还要小。Headscale 是一个单独的 Go 二进制程序,里面是一个存放设备、密钥和策略的小数据库;协调流量只是涓涓细流般的长轮询连接,每台设备一条,大多数时候几乎不携带任何数据。一台 Sentinel——2 vCPU、4 GB、120 GB NVMe,每月 $3.90——足以毫无压力地承载一个家庭、一家小公司,甚至几百台设备。只有当同一台机器还要兼职别的工作时,选型问题才真正变得有意思。内嵌 DERP 中继要转发真实流量,所以它要的是上行带宽而不是核心数。出口节点要转发设备的全部流量,所以它要的是上行带宽、宽裕的流量配额,以及一颗能跟上线速加密的 CPU。如果两者都打算用,就按流量来选型,把控制服务器本身当成可以忽略不计的舍入误差就好——或者更好的办法是,把出口节点放到第二台机器上,让控制平面继续保持无聊。

在自己的 VPS 上用出口节点,和用商业 VPN 是一回事吗?

技术上相似,但在隐私意义上几乎正好相反,值得看清楚。出口节点把设备的整条互联网连接路由经过你的 VPS,于是网站看到的是 VPS 地址,而不是你家里或酒店的地址,本地网络看到的也只是一条加密隧道,而不是你的浏览内容。到这一步,它和商业 VPN 是一样的。区别在于「人群」。商业 VPN 的地址由成千上万互不相关的客户共用,这正是它难以把流量归因到你身上的原因;而你出口节点的地址,永远只由你一个人使用,稳定不变。面对偷窥的咖啡馆网络,或是正在建立画像的 ISP,出口节点表现极佳。面对一个花几个月时间跨站点做关联分析的观察者,一个专属地址反而是持久的身份标识,共享地址则不是。把出口节点用在它擅长的地方——不受信任的本地网络、访问你自己的服务、需要一个稳定地址的场合——至于它覆盖不到的那部分威胁模型,去读旗舰指南。

我到底需要备份哪些东西?

四样东西,其中只有一样是显而易见的。数据库——默认是 SQLite,如果你迁移过就是 PostgreSQL——保存着用户、设备、公钥、路由和预授权密钥。首次启动生成的私钥材料是大家最容易忘记的部分:一旦丢失,所有设备都会拒绝这台已经信任了一年的服务器,也就意味着要手工把整支设备群重新注册一遍。配置文件之所以重要,是因为它记录着步骤 02 里那三个名字,而凭记忆去重建它们,正是人们不小心改动其中一个的原因。至于策略文件——如果你把访问规则保存在磁盘上——则是你最不想重写的东西。这四样加起来放在两个目录里,总重量比一张照片还轻。把它们加密,发送到一台不是这台机器的地方——最好是在另一个司法辖区,这正是备份指南要讲的内容——然后特意在一台备用 VPS 上完整还原一次,这样你才知道自己手里的文件就是需要的那份文件。

在 VPS 上同时运行控制服务器和出口节点,NordBastion 会介意吗?

控制服务器是一种普通、规矩的负载——一点 HTTPS、一点 UDP,几乎没有多少流量——可接受使用政策里没有任何一条会碰到它。出口节点值得认真想一句话,而不是一整段政策说教:设备通过它发出的一切流量,都会以你的 VPS 地址、在你的账户名下离开互联网,所以你的设备做什么,这个地址就会因此而被记住。这没什么问题,这本来就是这项功能的用途;但也正因如此,把出口节点分享给不认识的人是个坏主意,道理和开放中继一样。每个套餐的带宽都不限量,中继流量也只是普通流量,所以 DERP 服务器和出口节点都是随附功能,不额外计费。除此之外:账户不需要你的真名,账单以门罗币或比特币结算,而这台保存着你名下每一台设备地图的机器,也不会被任何人索引收录。

获取这台机器

一套完全属于你自己的控制平面,挂在一个不用你真名的账户下。无需 KYC,加密货币支付。

Sentinel——2 vCPU、4 GB,每月 $3.90,可选赫尔辛基、斯德哥尔摩、奥斯陆或雷克雅未克。足以运行 Headscale、Caddy 和你自己的 DERP 中继,如果还想在同一台机器上跑出口节点,也绰绰有余。注册无需邮箱,无需身份证件,也没有公司数据库保存着你名下每一台设备的清单。

最后审核 · 2026-10-12 · 来源 · Headscale 文档(配置参考、客户端设置、ACL、DERP、远程 CLI)、Tailscale 客户端与出口节点文档、WireGuard 协议论文、Caddy 文档 · 周期 · 每年

私密托管,源自北欧。