从零搭建低延迟虚拟局域网:阿里云 ECS + Headscale + 自建 DERP
阿里云ECS与headscale与游戏联机
动机
-
游戏联机卡卡的:和朋友玩Minecraft卡得连东西都吃不动?玩幻兽帕鲁联机延迟高得路都走不了?玩泰拉瑞亚看大家都在瞬移?
-
现有的局域网解决方案呢?:樱花内网穿透软件隧道靠运气、Radmin VPN和tailscale一远就爆炸、Astral和EasyTier等跨平台游戏联机软件服务器寄人篱下、乔尔加速器和米西开黑这种高度封装商业化的联机软件要爆米。有没有什么
嘉豪更有技术含量并且可以把联机命运牢牢把我在自己手上的方法? -
我租服务器只用于学习不是白租了吗:租个阿里云服务器只为了做课程设计、比赛项目以及其他学习用途,怎么想都浪费了,学习是一时的,玩游戏爽是一辈子的。学个计算机不能让自己更爽地在电脑上玩游戏,那我计算机不是白学了吗?旧的闲置的服务器不要上转转,赶紧利用起来搭虚拟局域网。
所以我决定用我的ubuntu ECS服务器把我的几个要联机的windows电脑连到一个虚拟局域网内。
技术路线
理论基础
感谢GPT老师指路,由于我对tailscale最熟所以我选了headscale这种方法,另外还有 开源工具Nebula、ZeroTier等方法等着去探索,
我懒我就不探索了。
已知tailscale有三种流量路径:

Tailnet 内部的数据包100.x.x.x之间,Tailscale 会:
- 能直连就直连(UDP Hole Punch)
- 不能直连就走官方 DERP
不会说:
"哦,你还有一台阿里云,我帮你绕一下。"
官方没有这个功能。
当然不得不提的是用第三种方法可以用tailscale搭建VPN。
方案
像tailscale这种国外组网应用,大部分都有这个通病——官方DERP服务器在国外,P2P通讯一旦失效就无法连接。利用官方DERP服务器从北京绕到东京再绕回上海显然会卡,那么一个显而易见的方法就是把中间东京的服务器改成自己的国内服务器,既能低成本降低延迟又不会占用服务器过多负载,最大的成本或许是公网流量了。
需要判断自己通过tailscale访问其他设备是使用什么方式可以通过以下指令
tailscale ping 100.xxx.xxx.xxx
pong from desktop-up8bsvt (100.xxx.xxx.xxx) via DERP(tok) in 392ms
意味着我正在通过东京的服务器和朋友联机,两台机器ping加起来延迟接近一秒了,显然这是不可接受了。
官方 Tailnet 默认仍然会优先使用官方 DERP,因此只在自己的 ECS 上启动一个 derper,并不会自动生效。所以我们选择Headscale + 自建 DERP,从而实现。
- Headscale 自带嵌入式 DERP,正好能通过同一个 Headscale 容器提供中继,维护简单,完全控制 DERP
- 优先利用自建 DERP,保留官方 DERP 作为候选节点
- 国内延迟通常几十毫秒
最终的技术方案如下:
- 使用 Docker Compose 部署 Headscale;
- 启用 Headscale 内嵌 DERP;
- 配置 HTTPS;
- 把 Windows 客户端迁移到自己的 Headscale 网络。
实现
网关
HTTPS(443)请求 \(\rightarrow\) Nginx 反向代理 \(\rightarrow\) Headscale Docker 容器端口(8080,如果觉得太常用了可以自己换)
公网 UDP (3478) \(\rightarrow\) Headscale DERP/STUN
熟悉阿里云网关设置的可以直接跳到Headscale 部署,不过headscale的Nginx配置和HTTPS直接建网站还是有区别。
网关设置
-
在实例的安全组配置入方向的规则

-
为自定义的UDP流量分配端口,允许游戏联机的UDP流量走自己的服务器

-
HTTPS配置:
443端口的分配一般云服务器都会自动完成,操作方式和UDP类似,区别是HTTPS用的TCP。更多的配置是在Nginx层反代理实现的。
域名与Nginx配置
-
域名:
不要直接 www 开头或其他主域名,因为它很可能已经承载原有网站或其他功能。新增两个 DNS 解析会更清晰,这里由于我们这个域名专门用于headscale所以我直接用hs.xxx.cn。 -
Nginx:
先在设置/etc/nginx/sites-available/hs.xxx.cn中写一个临时的站点用来申请SSL证书
server {
listen 80;
listen [::]:80;
server_name hs.xxx.cn;
location = /generate_204 {
return 204;
}
location / {
return 200 "hs.xxx.cn is ready\n";
add_header Content-Type text/plain;
}
}
并创建软链接来启用站点
sudo ln -s /etc/nginx/sites-available/hs.xxx.cn /etc/nginx/sites-enabled/hs.xxx.cn
申请证书并在Nginx中自动生成配置
sudo certbot certonly --nginx -d hs.xxx.cn
增加 Nginx 全局 map
Headscale 客户端不是只使用普通 WebSocket,它还有两个特殊点:
- 使用 POST 发起 Upgrade;
- Upgrade 值是
tailscale-control-protocol。
因此 Nginx 必须原样转发 Upgrade 请求。
打开 nano /etc/nginx/nginx.conf,在 http { 内部、任意 server 块之外加入:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
如果现有配置已经存在同名 $connection_upgrade,不要重复添加。
为Nginx添加最终配置
在配置完SSL证书之后我们的站点配置 /etc/nginx/sites-available/hs.xxx.cn 多了一个证书的server块,现在我们把这个SSL的server块替换为:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name hs.xxx.cn;
ssl_certificate /etc/letsencrypt/live/hs.xxx.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/hs.xxx.cn/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:HEADSCALE_SSL:10m;
ssl_session_timeout 1d;
location / {
proxy_pass http://headscale_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header True-Client-IP $remote_addr;
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 10s;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}
}
同时在配置最开始添加一个上游的后端:
upstream headscale_backend {
zone headscale_backend 64k;
server 127.0.0.1:8080 max_fails=1 fail_timeout=5s;
keepalive 2;
}
中间的server块是http设置,按需求保留或修改即可。
最后别忘了使用
sudo nginx -t
sudo certbot certificates
检查自己的证书和Nginx设置是否成功
syntax is ok
test is successful
Found the following certs:
Certificate Name: hs.xxx.cn
Serial Number: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key Type: ECDSA
Domains: hs.xxx.cn
Expiry Date: 2026-12-22 09:05:55+00:00 (VALID: 53 days)
Certificate Path: /etc/letsencrypt/live/hs.xxx.cn/fullchain.pem
Private Key Path: /etc/letsencrypt/live/hs.xxx.cn/privkey.pem
配置没有问题之后重启Nginx使配置生效即可sudo systemctl reload nginx。
最后在公网外ping以下测试是否成功
curl -v https://hs.xxx.cn/health
StatusCode : 200
StatusDescription : OK
Content : {123, 34, 115, 116...}
RawContent : HTTP/1.1 200 OK
Connection: keep-alive
Content-Security-Policy: frame-ancestors 'none'
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Length: 18
Cont...
Headers : {[Connection, keep-alive], [Content-Security-Policy, frame-ancestors 'none'], [Referrer-Policy, no-referrer], [X-Content-Type-Options, nosniff]...}
RawContentLength : 18
Headscale 部署
所有配置请以官方仓库为基准,本博客仅展示docker拉取某一具体版本的操作,以及
v0.29.2版本的headscale配置参考。
docker拉取
- 先创建Headscale的目录
sudo mkdir -p /opt/headscale/{config,lib}
sudo chown -R root:root /opt/headscale
cd /opt/headscale
- 创建 Docker Compose
拉取前先确认headscale版本docker run --rm ghcr.io/juanfont/headscale:latest version,下面为我的配置:
- Headscale:
v0.29.2 - 镜像:
ghcr.io/juanfont/headscale:latest /opt/headscale/config为空/opt/headscale/lib为空
确认版本之后不要继续长期使用 latest,因为以后重新拉取可能跨版本。
sudo nano /opt/headscale/compose.yaml
services:
headscale:
image: ghcr.io/juanfont/headscale:0.29.2
container_name: headscale
restart: unless-stopped
read_only: true
tmpfs:
- /var/run/headscale
ports:
- "127.0.0.1:8080:8080"
- "127.0.0.1:9090:9090"
- "3478:3478/udp"
volumes:
- /opt/headscale/config:/etc/headscale:ro
- /opt/headscale/lib:/var/lib/headscale
command: serve
healthcheck:
test: ["CMD", "headscale", "health"]
interval: 30s
timeout: 10s
retries: 3
这里:
8080:Headscale 控制服务,仅供本机 Nginx 使用;9090:监控端口,仅供本机使用;3478/udp:内置 DERP 的 STUN 服务,必须公网开放。Headscale 官方说明,启用内置 DERP 后需要公开 UDP 3478。
修改完docker配置之后再重新拉取以固定版本
cd /opt/headscale
sudo docker compose pull
da0100c58f75: Pull complete
Digest: sha256:d337f1be4a9155b330aa9077bf6c82d24ff0581b8e69390ebc6d7c623bb339ce
Status: Downloaded newer image for ghcr.io/juanfont/headscale:latest
ghcr.io/juanfont/headscale:latest
配置headscale
下载与 v0.29.2 对应的配置模板 ,配置模板应当和运行版本使用相同 tag,不能直接拿 main 分支模板
sudo curl -fL \
https://raw.githubusercontent.com/juanfont/headscale/v0.29.2/config-example.yaml \
-o /opt/headscale/config/config.yaml
然后备份配置文件
sudo cp /opt/headscale/config/config.yaml \
/opt/headscale/config/config.yaml.original
之后就可以在/opt/headscale/config/config.yaml中修改headscale配置了,官方的示例已经讲得很清楚,这里介绍几个比较重要的字段:
服务url
server_url: https://hs.xxx.cn
Headscale 监听地址
由于 Headscale 在 Docker 容器内,必须监听 0.0.0.0:8080;宿主机层面已经通过 Compose 限制为:
listen_addr: 0.0.0.0:8080
所以不会直接暴露公网。
Metrics
metrics_listen_addr: 0.0.0.0:9090
gRPC
也是一种协议,但是和玩游戏没啥关系
grpc_listen_addr: 0.0.0.0:50443
TLS
Headscale 本身不处理证书,HTTPS 交给 Nginx:
tls_cert_path: ""
tls_key_path: ""
这种情况下,server_url 仍然必须写成 HTTPS;官方说明启动时出现“以无 TLS 方式监听,但 ServerURL 使用 HTTPS”的警告属于正常现象,因为 TLS 由反向代理终止。
可信代理
trusted_proxies:
- 172.16.0.0/12
- 127.0.0.1/32
- ::1/128
Docker Bridge 网络通常会从 172.16.0.0/12 范围连接容器。这样 Headscale 能读取 Nginx 转发过来的真实客户端 IP,可以按实际 Docker 网络收窄可信范围。
数据库配置
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
/var/lib/headscale/db.sqlite是宿主机的持久化目录
配置内置 DERP
derp:
server:
enabled: true
region_id: 999
region_code: "aliyun-cn"
region_name: "Aliyun China DERP"
verify_clients: true
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: 你的ECS公网IPv4
ipv6: ""
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 3h
注意urls字段是 externally available DERP maps,即备用的DERP服务器,这里把tailscale官方服务器放在这,如果确定自己的服务器稳定能用后可以再自己决定要不要移除
检查 IP 地址池
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
不要改成普通局域网地址。
Headscale/Tailscale 客户端对这两个地址段有固定假设
暂时关闭 MagicDNS 或使用安全默认值
为了先把 Headscale 和 DERP跑通,可以使用:
dns:
magic_dns: true
base_domain: tail.xxx.cn
base_domain 不需要真的配置公网 DNS,它用于 Tailnet 内部 MagicDNS 名称。
以后机器可能获得类似:alpharaway.tail.xxx.cn、desktop-up8bsvt.tail.xxx.cn
检查并启动
检查配置是否成功
sudo docker run --rm \
-v /opt/headscale/config:/etc/headscale:ro \
-v /opt/headscale/lib:/var/lib/headscale \
ghcr.io/juanfont/headscale:0.29.2 \
configtest
出现Configuration is valid就可以启动headscale了
初次检查的时候可能会因为某些配置文件还没创建而输出别的内容,这是正常现象
sudo docker run --rm \
> -v /opt/headscale/config:/etc/headscale:ro \
> -v /opt/headscale/lib:/var/lib/headscale \
> ghcr.io/juanfont/headscale:0.29.2 \
> configtest
2026-07-25T03:25:52Z INF no private key file at path, creating... path=/var/lib/headscale/noise_private.key
2026-07-25T03:25:52Z INF Opening database database=sqlite3 path=/var/lib/headscale/db.sqlite
2026-07-25T03:25:52Z INF no private key file at path, creating... path=/var/lib/headscale/derp_server_private.key
注意两个以.key为后缀的文件都是重要的服务身份数据,不要随意泄露。
启动docker容器
cd /opt/headscale
sudo docker compose up -d
下面是一些检测容器状态的指令,正常来说没有异常那headscale就是启动成功了。
检查docker容器以及容器内部日志
sudo docker compose ps
sudo docker compose logs --tail=100 headscale
检查服务器端口配置
curl -v http://127.0.0.1:8080/health
sudo ss -lntup | grep -E ':8080|:9090|:3478'
tailscale连接到headscale服务器
使用自己的服务器就不需要登录自己的帐号了,而是在自己的headscale服务器上验证
在容器中创建统一用户
headscale users create alpha
如果你不知道怎么在docker容器中执行指令,那就在服务器上执行
sudo docker exec headscale headscale users create alpha
查看用户的编号
sudo docker exec headscale headscale users list
创建一次性预认证密钥
sudo docker exec headscale \
headscale preauthkeys create \
--user 1
服务器会生成一个密钥,复制这个密钥,然后在你想要连接至 alpha 用户的windows电脑上安装tailscale(安装完之后千万不要登录,因为你要登录到headscale服务器而不是官方),在windows电脑终端里运行:
tailscale up --login-server=https://hs.xxx.cn --auth-key=刚才生成的密钥
这个密钥默认是一次性使用且短时间有效的。所以为了在第二台windows上链接到服务器需要再生成一次密钥,登录操作是一样的,这个密钥可以理解为允许一台新设备加入这个 Tailnet 的一次性邀请码。
注册完成以后:
- 这个 Key立即失效;
- 以后重启电脑、重新连接,都不需要再次输入;
- Headscale 会记住这台设备。
登录成功的tailscale是这样的:

联机
以防有人因为不知道幻兽帕鲁可以局域网联机而被官方服务器整的死去活来,这里分享一点利用headscale虚拟局域网联机幻兽帕鲁有效降低延迟的实例。
启动服务器
steam购买幻兽帕鲁之后在工具里会有自定义服务器软件,安装即可:

这里不展示服务器配置方法,关于服务器配置以及如何把存档迁移到Dedicated服务器的方法,我以后来兴致了可能会再单独出个博客(画饼 \(\times\) 1),如果想开新档按我这里的操作已经够用了。
注意steam运行之后不要关闭终端,服务器是运行在终端而不是windows后台。
客户端连接

这里腐竹可以利用localhost连接查看服务器是否启动成功,如果启动成功就可以让其他小伙伴用腐竹电脑的tailscale ip联机,端口保持默认 8211 即可。
写在最后
麻烦但是一劳永逸的组网方式,再也不用每次连接都去折腾软件了,反正开机tailscale自动启动就帮你把局域网组好了,但是启动tailscale偶尔会导致https访问网页卡顿,目前还不知道原因是什么,觉得卡把tailscale关掉就行了,之后查明理由了可能再出一期博客(画饼 \(\times\) 2)。

浙公网安备 33010602011771号