本文讲述了基于Debian使用开源的headscale服务端和tailscale为多个设备建立虚拟内网直连,配置web-ui管理界面,以及建立私有的DERP中转服务器,通过MagicDNS分配主机域名,还包含了多用户的ACL访问控制。
背景Tailscale 是基于 WireGuard 的虚拟组网工具,它由协调服务器(下发配置和密钥)、DERP 中继服务器(当直连失败时代替中转流量)和客户端节点三部分组成。Headscale 是 Tailscale 协调服务器的开源实现,让你完全掌控自己的虚拟内网,不受官方设备数量和用户限制。
参考文章
- BILIBILI: 自建内网穿透headscale,免费开源版tailscale无限制使用
- 对应文章: 开源版tailscale,headscale搭建
- BILIBILI: Derp服务搭建 让Tailscale/HeadScale飞起来
- BILIBILI: 给Headscale搭建一个内网穿透设备管理界面(headscale-ui)
前提需要一个具有公网IP的服务器用来搭建Headscale服务端,并且强制要求有一个域名用于HTTPS通信,如果
不使用80和443端口或非中国大陆服务器,则可以不需要备案。
环境讲解都以我的环境为例:
- 服务端:阿里云服务器 Debian 12
- 客户端:Debian
服务端:Headscale配置
安装
headscale来自一个开源项目,你可以直接前往github release下载deb或脚本安装
以我写文章时的最新版headscale_0.28.0为例,你可以前往github查找最新的并替换以下的下载链接
Debian系统
直接下载deb包安装
wget https://github.com/juanfont/headscale/releases/download/v0.28.0/headscale_0.28.0_linux_amd64.debsudo apt install ./headscale_0.28.0_linux_amd64.deb使用apt安装会自动配置好目录和用户,非常方便。
其他发行版
# 下载服务端(请将版本号替换为最新版)wget --output-document=/usr/local/bin/headscale https://github.com/juanfont/headscale/releases/download/v0.28.0/headscale_0.28.0_linux_amd64# 设置执行权限chmod +x /usr/local/bin/headscale手动配置:
#创建配置目录:mkdir -p /etc/headscale
#创建目录用来存储数据与证书:mkdir -p /var/lib/headscale
#创建空的 SQLite 数据库文件:touch /var/lib/headscale/db.sqlite
# 从example 创建 Headscale 配置文件:wget https://github.com/juanfont/headscale/raw/main/config-example.yaml -O /etc/headscale/config.yaml
# 创建 headscale 用户:
useradd headscale -d /home/headscale -m
# 修改 /var/lib/headscale 目录的 owner:
chown -R headscale:headscale /var/lib/headscale配置文件设置
配置文件位置在/etc/headscale/config.yaml,可按照以下修改。
server_url:客户端连接的url,建议创建一个子域名,设置为https://tailvpn.<主域名>:443,也可选择其他端口,但客户端连接时填入的url要与其完全一致。listen_addr:服务监听端口(内网),设置为127.0.0.1:8080,端口随意设置,只要不冲突即可。prefixes:自定义虚拟内网ip,可随意填写,注意不要与真实ip冲突,建议避开192.168.x.xv4: 建议修改成:100.64.0.0/16,避免与阿里云内网冲突v6: 建议保持默认:fd7a:115c:a1e0::/48
derp:内置的DERP服务,建议保持关闭,后文要搭建独立的DERP服务。dns:magicDNS配置magic_dns:默认配置为truebase_domain:设置一个自己的子域名,可以是tailnet.<主域名>,不能和上文的server_url重复,自己随便起一个,不经过公共DNS解析。nameservers:公网服务DNS,非常重要,在global:下添加两条国内DNS,否则可能导致无法上网,并且移除其他的dns。- 223.5.5.5- 223.6.6.6
示例配置文件片段
51 collapsed lines
......server_url: https://tailvpn.<主域名>:443
# Address to listen to / bind to on the server## For production:# listen_addr: 0.0.0.0:8080listen_addr: 127.0.0.1:8080
......
prefixes: v4: 100.64.0.0/16 v6: fd7a:115c:a1e0::/48
......
derp: server: # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place enabled: false
......
dns: # Whether to use MagicDNS magic_dns: true
# Defines the base domain to create the hostnames for MagicDNS. # This domain _must_ be different from the server_url domain. # `base_domain` must be a FQDN, without the trailing dot. # The FQDN of the hosts will be # `hostname.base_domain` (e.g., _myhost.example.com_). base_domain: tailnet.<主域名>
# Whether to use the local DNS settings of a node or override the local DNS # settings (default) and force the use of Headscale's DNS configuration. override_local_dns: true
# List of DNS servers to expose to clients. nameservers: global: - 223.5.5.5 - 223.6.6.6 # - 1.1.1.1 # - 1.0.0.1 # - 2606:4700:4700::1111 # - 2606:4700:4700::1001
......创建systemd服务
sudo vim /etc/systemd/system/headscale.service[Unit]Description=headscale controllerAfter=syslog.targetAfter=network.target
[Service]Type=simpleUser=headscaleGroup=headscaleExecStart=/usr/bin/headscale serve # 也有可能是 /usr/local/bin/headscaleRestart=alwaysRestartSec=5
# Optional security enhancementsNoNewPrivileges=yesPrivateTmp=yesProtectSystem=strictProtectHome=yesReadWritePaths=/var/lib/headscale /var/run/headscaleAmbientCapabilities=CAP_NET_BIND_SERVICERuntimeDirectory=headscale
[Install]WantedBy=multi-user.target编辑后配置并启动服务:
# Reload SystemD 以加载新的配置文件:
systemctl daemon-reload
#启动 Headscale 服务并设置开机自启:
systemctl enable --now headscale
# 查看运行状态:
systemctl status headscale
# 查看占用端口:
ss -tulnp|grep headscale
# 创建一个用户,以便后续客户端接入,例如:headscale users create default
# 查看用户列表:
headscale users list配置ssl证书
sudo apt install certbot申请证书,注意此时服务器上的80端口必须是未被占用状态,否则失败,如果被nginx占用,先输入sudo nginx -s stop停止服务。
以下的域名都应该是headscale配置文件中server_url填写的域名。
sudo certbot certonly --standalone -d 域名接下来会有两个问题:
- 第一个选择:同意服务条款
- 输入 Y 然后回车。这是申请的强制性要求,没有任何风险,因为你必须同意才能继续获取免费证书。
- 第二个选择:是否与 EFF 共享邮箱
- 这是一个完全可选的选择,建议输入
N然后回车。如果你拒绝共享 (N):你不会因此收到Let’s Encrypt的续期或安全提醒邮件。这部分重要通知只与你注册时填写的邮箱有关。
- 这是一个完全可选的选择,建议输入
nginx配置反向代理
新建一份配置文件,默认在/etc/nginx/conf.d/下,nginx会自动引入。
sudo vim /etc/nginx/conf.d/headscale.conf文件示例:
server { listen 443 ssl; server_name 域名; charset utf-8;
ssl_certificate /etc/letsencrypt/live/域名/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/域名/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 5m; keepalive_timeout 70;
location / { proxy_redirect off; proxy_pass http://127.0.0.1:8080; # port proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}填写配置文件
- headscale配置文件
server_url字段:如https://tailvpn.<主域名>:443- 填写:
listen 443 ssl;server_name tailvpn.<主域名>;
- 填写:
- headscale配置文件
listen_addr字段:如127.0.0.1:8080- 填写:
proxy_pass http://127.0.0.1:8080;
- 填写:
然后重启nginx
# 测试文件语法是否正确sudo nginx -t
# 启动nginxsudo nginx# 重新加载配置文件sudo nginx -s reloadNOTE服务端的必要配置已完成,Headscale默认使用官方DERP服务器也可以工作,如果你想快速使用,请直接跳到客户端tailscale配置
公共服务器带宽有限用的人多了就体验很差,而且流量需要经过第三方的服务器,不够安全和稳定,这就需要我们自行搭建DERP服务器。
服务端:DERP服务器配置
NOTE节点与节点进行连接时,会首先通过DERP服务进行中转连接,以此让穿透立刻实现——中转服务可以保证 100% 连接成功。
然后,DERP 服务会尝试协助两个节点进行点对点直连(NAT 穿透)。如果直连成功,DERP 不再中转数据;否则流量会持续走中继。
以下情况直连容易失败,强烈建议自建 DERP:
- 节点双方都在对称型 NAT 后面(如移动 4G/5G 热点、公司防火墙)
- 节点之间** UDP 被阻断**
- 需要跨境访问
IMPORTANT请确保你有Docker或者Podman,原文中使用的是
Docker镜像安装,我使用的是其开源替代版本Podman,如果你的是Docker,直接把以下命令中的podman换成docker即可,完全兼容。
安装
podman run --restart always \ --name derper -p 81:12345 -p 3478:3478/udp \ -e DERP_ADDR=:12345 \ -e DERP_DOMAIN=<域名> \ -d swr.cn-north-4.myhuaweicloud.com/ddn-k8s/ghcr.io/yangchuansheng/derper:latest此处的<域名>可以和headscale配置文件server_url字段的域名相同,端口不同即可,同nginx配置,还填写tailvpn.<主域名>即可。
端口81是服务监听地址,和接下来nginx的配置需要一致。
nginx配置
NOTE若是不同的子域名,需要独立配置证书,见配置ssl证书,若是相同子域名可直接继续。
新建一份配置文件,默认在/etc/nginx/conf.d/下,nginx会自动引入。
sudo vim /etc/nginx/conf.d/derper.confserver { listen 8443 ssl; server_name 域名; charset utf-8;
ssl_certificate /etc/letsencrypt/live/域名/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/域名/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; ssl_prefer_server_ciphers on; ssl_session_cache shared:SSL:10m; ssl_session_timeout 5m; keepalive_timeout 70;
location / { proxy_redirect off; proxy_pass http://127.0.0.1:81; # port proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }}修改headscale配置
新建一个文件在/etc/headscale/derp.yaml
sudo vim /etc/headscale/derp.yamlregions: 900: regionid: 900 regioncode: awh regionname: AliWuHan nodes: - name: 900a regionid: 900 hostname: 域名 # ipv4: ip stunport: 3478 # stunonly: false derpport: 8443regioncode:名称简写,可自定义。AliWuHan:DERP服务器全称,可自定义。hostname:DERP服务器域名,填写安装镜像和nginx中配置的相同域名。derpport:DERP服务器端口,填写nginx中配置的相同端口。
IMPORTANT别人只要知道了你的域名和端口,就可以白嫖你的derper服务,这一点请知晓
在主配置文件中添加自定义derp
sudo vim /etc/headscale/headscale.yaml修改成下面这样
# Locally available DERP map files encoded in YAML # # This option is mostly interesting for people hosting their own DERP servers: # https://tailscale.com/docs/reference/derp-servers/custom-derp-servers # https://headscale.net/stable/ref/derp/ # # paths: # - /etc/headscale/derp-example.yaml paths: [/etc/headscale/derp.yaml]# 重启服务sudo systemctl restart headscale在客户端验证 DERP 连通性:
# 查看节点即可tailscale netcheck服务端:Headscale-UI配置(可选)
Headscale-UI 提供了一个网页管理界面,方便查看节点状态和配置。不过日常使用中命令行已经足够,按需安装即可。

直接前往github release下载zip,本质上是一个静态网页,所以直接解压,配置nginx即可。
修改nginx配置文件
sudo vim /etc/nginx/conf.d/headscale.conf # 在location /后添加 location /web { alias /home/mint/web/headscale; try_files $uri $uri/ index.html; }将/home/mint/web/headscale修改为你解压的目录即可。
如何登录
通过浏览器访问https://域名/web即可显示页面。

以上页面需要配置apikey,在服务器执行以下命令,复制到浏览器保存即可。
headscale apikeys create -e 720d客户端:Tailscale配置
安装Tailscale
官方安装
以下以debian系统为例:
curl -fsSL https://tailscale.com/install.sh | sh镜像包安装
curl --output ./tailscale_1.98.3.deb https://mirrors.lzu.edu.cn:8080/tailscale/debian/pool/tailscale_1.98.3_amd64.deb
sudo apt install ./tailscale_1.98.3.deb登录tailscale
sudo tailscale up --login-server=https://域名 \ --accept-dns \ --netfilter-mode=on \ --operator=<用户名> \ --accept-routes=false--login-server:填写headscale中的server_url--accept-dns:接管系统DNS,如果上网出现问题,请设置--accept-dns=false,但是DERP和MagicDNS会失效。--netfilter-mode:接管系统路由表,保持默认开启即可,阿里云服务器上的tailscale客户端需要关闭--netfilter-mode=off避免内网冲突。--operator:用户名,每个用户的访问独立,一般一个人使用就填写统一的用户名即可,在headscale服务器处需要填写相同的用户名。--accept-routes:开启子网路由,可通过此设备访问该设备下的所有设备,建议关闭--accept-routes=false。
如果用户名不存在,先在服务器上创建:
headscale user create <用户名>回车后正常情况下会出现:
To authenticate, visit:
https://域名/register/EOVti6kj9dVEZinNGv2RL58D点击连接在浏览器打开:

复制浏览器上显示的命令,在Headscale服务器上执行该命令,修改USERNAME为你申请的用户名,客户端显示success即为成功。
Tailscale常用命令
# 显示节点状态tailscale status
# 下线tailscale down# 上线tailscale up# 退出登录(需要重新认证)tailscale logout
# 检测网络(查看DERP)tailscale netcheck# 检测节点连接状态tailscale ping <节点名># 检查DNS状态tailscale dns statusHeadscale常用命令
# 列出所有节点headscale nodes list# 列出所有路由节点headscale nodes list-routes
# 设置标签headscale nodes tag -i <id> -t tag:shared-server# 重命名节点sudo headscale nodes rename -i <id> <新名字># 删除节点sudo headscale nodes delete -i <id>Headscale ACL访问控制
NOTEHeadscale 实现了与 Tailscale 相同的 ACL 策略,遵循最小权限和零信任原则。ACL 默认是拒绝所有的——只有明确定义的规则才会放行流量。如果你不配置任何 ACL 文件,则默认允许所有设备互相通信。
参考文档
- Headscale ACL: https://headscale.net/stable/ref/acls/
- Tailscale ACL 语法: https://tailscale.com/kb/1018/acls/
启用 ACL
在 config.yaml 中指定 ACL 策略文件路径:
sudo vim /etc/headscale/config.yaml# Path to ACL filepolicy: path: /etc/headscale/acl.hujson然后创建 ACL 文件:
sudo vim /etc/headscale/acl.hujsonIMPORTANTACL 文件必须使用 huJSON 格式(支持注释和尾部逗号的 JSON)。文件名可以自定,但建议使用
.hujson扩展名以明确格式。每次修改 ACL 文件后需要重载 Headscale 才能生效。
重载方式:
# 方法一:systemd 重载sudo systemctl reload headscale
# 方法二:发送 SIGHUP 信号sudo kill -HUP $(pidof headscale)ACL 基本结构
ACL 策略文件由以下几个核心部分组成:
| 字段 | 说明 |
|---|---|
groups | 用户组,将多个用户归为一组方便管理 |
tagOwners | 标签所有者,定义谁可以为节点打上指定标签 |
hosts | 主机别名,用自定义名称代替 IP 地址 |
acls | 访问规则,定义 src → dst 的允许规则 |
每条 ACL 规则的基本格式:
{ "action": "accept", "src": ["<源>"], "dst": ["<目标>:<端口>"]}src:流量来源,可以是用户(用户名@)、组(group:组名)、标签(tag:标签名)或自动组dst:流量目标,格式为目标:端口,端口可用*表示所有端口,多个端口用逗号分隔proto(可选):协议类型,如tcp、icmp,不指定则允许所有协议
简单示例
允许所有(默认行为)
如果定义了 ACL 文件但 acls 字段为空对象,等同于允许所有流量:
//file: /etc/headscale/acl.hujson
{}拒绝所有
设置空的 acls 数组,禁止所有设备间通信:
//file: /etc/headscale/acl.hujson
{ "acls": []}实用配置示例
以下是一个适合个人/小团队的配置,假设你有一个共享服务器和一些个人设备:
//file: /etc/headscale/acl.hujson
{ // 定义用户组 "groups": { "group:admin": ["<你的用户名>@"] },
// 定义哪些用户可以为节点打标签 "tagOwners": { "tag:shared-server": ["group:admin"], "tag:home-device": ["group:admin"] },
// 主机别名(用 MagicDNS 域名或 IP) // 此处可选,直接使用 MagicDNS 名称即可,无需定义 hosts
"acls": [ // 所有个人设备可以互相访问所有端口 { "action": "accept", "src": ["group:admin"], "dst": ["group:admin:*"] },
// 所有个人设备可以访问带 tag:shared-server 标签的服务器 { "action": "accept", "src": ["group:admin"], "dst": ["tag:shared-server:*"] },
// 共享服务器之间也可以互相访问(如数据库连接) { "action": "accept", "src": ["tag:shared-server"], "dst": ["tag:shared-server:*"] } ]}为节点打标签
节点注册时添加标签,后续 ACL 规则即可通过标签匹配:
# 注册时就指定标签sudo tailscale up --advertise-tags=tag:shared-server ...
# 或者事后通过 headscale 命令添加标签headscale nodes tag -i <节点ID> -t tag:shared-serverNOTE标签的生效需要
tagOwners中明确授权——你只能给节点打上自己被授权管理的标签。
自动组(Autogroups)
Headscale 提供几个内置自动组,无需手动维护成员列表:
| 自动组 | 说明 | 可用位置 |
|---|---|---|
autogroup:member | 所有个人设备(未打标签的设备) | src / dst |
autogroup:tagged | 所有打了标签的设备 | src / dst |
autogroup:internet | 通过出口节点访问互联网 | 仅 dst |
autogroup:self | 同一用户认证的源和目标设备(不含标签设备) | 仅 dst |
常用自动组规则
{ "acls": [ // 允许个人设备访问自己的其他设备 { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"] },
// 允许个人设备访问所有打了标签的服务器 { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:tagged:*"] },
// 允许通过出口节点访问互联网 { "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:internet:*"] } ]}CAUTION
autogroup:self在大量节点的部署中可能导致性能下降,因为过滤规则需要按节点编译而非全局编译。如果性能有问题,可以改为显式列出每个用户的规则。
端口限制示例
只开放特定端口,提高安全性:
{ "acls": [ // 只允许 SSH 和 HTTPS 访问服务器 { "action": "accept", "src": ["group:admin"], "proto": "tcp", "dst": ["tag:shared-server:22,443"] },
// 允许 ping 服务器 { "action": "accept", "src": ["group:admin"], "proto": "icmp", "dst": ["tag:shared-server:*"] } ]}我的实际配置
以下是我在 R730 服务器上实际使用的 ACL 配置,供参考:
{ // 定义哪些用户/用户组可以给设备打标签 "tagOwners": { "tag:shared-server": ["mint@"], "tag:shared-vm": ["mint@"], },
// 定义访问控制规则 "acls": [ { "action": "accept", // 访问源可以是网络内的所有普通用户设备,或是特定用户 "src": ["autogroup:member"], // 允许源设备访问"自己"名下的所有设备的所有端口 "dst": ["autogroup:self:*"] }, // 精确控制(只开放 5901 端口) { "action": "accept", // 访问源:你想要授权的好友用户(注意:用户名后需要加 @) "src": ["cyan@"], // 目的地:你的共享设备 的 5901 端口 "dst": ["tag:shared-server:5901"] }, { "action": "accept", "src": ["mint@"], "dst": ["tag:shared-server:*"] // 允许访问所有端口 }, { "action": "accept", "src": ["autogroup:member"], "dst": ["tag:shared-vm:*"] } ]}规则逐条解析
① 个人设备互访
{ "action": "accept", "src": ["autogroup:member"], "dst": ["autogroup:self:*"] }所有未打标签的个人设备(autogroup:member)可以访问自己名下的其他设备。例如你的笔记本和手机都登录了同一个 mint 用户,它们之间可以互相访问所有端口,但mint的设备不能访问cyan的设备。
② 好友精确授权(VNC 端口)
{ "action": "accept", "src": ["cyan@"], "dst": ["tag:shared-server:5901"] }为好友 cyan 仅开放 服务器的 5901 端口(VNC 远程桌面)。这条规则体现了最小权限原则——cyan 只能看到这一个端口,无法访问服务器上的其他服务。
③ 管理员全端口访问
{ "action": "accept", "src": ["mint@"], "dst": ["tag:shared-server:*"] }你自己的用户 mint 可以对 tag:shared-server 标签的服务器访问所有端口(SSH、HTTP、数据库等),方便管理。
④ 所有人可访问共享虚拟机
{ "action": "accept", "src": ["autogroup:member"], "dst": ["tag:shared-vm:*"] }所有内网成员都可以访问带有 tag:shared-vm 标签的虚拟机全部端口。适用于团队共享的开发环境或测试机。
标签划分思路
我使用两个标签来区分不同安全级别的设备:
| 标签 | 用途 | 访问策略 |
|---|---|---|
tag:shared-server | 核心服务器(R730) | 仅管理员全权限,好友按端口授权 |
tag:shared-vm | 共享虚拟机 | 所有内网成员均可访问 |
设备注册时打上对应标签:
# R730 服务器sudo tailscale up --advertise-tags=tag:shared-server ...
# 共享虚拟机sudo tailscale up --advertise-tags=tag:shared-vm ...验证与调试
修改 ACL 后查看日志确认是否加载成功:
# 查看 headscale 日志,确认 ACL 解析结果sudo journalctl -u headscale -f | grep -i acl如果 ACL 文件格式有误,Headscale 会在日志中输出具体错误信息,修改后重载即可。
验证部署效果
所有步骤完成后,在客户端上运行以下命令确认一切正常:
# 查看当前节点状态(应显示 direct 而非 relay)tailscale status
# 检测 DERP 连通性(应看到你的自定义 DERP 节点)tailscale netcheck
# MagicDNS 是否生效ping <设备名>.tailnet.<你的域名>
# DNS 状态tailscale dns status如果 tailscale status 中节点显示为 relay 而非 direct,说明直连未成功,检查 DERP 是否正常工作、双方防火墙是否允许 UDP。
以下是可爱的评论们:

输入用户名和邮箱后自动检查登录状态。登录后用户名和邮箱将被绑定, 只可以修改头像和主页链接。