运维| headscale自建点对点异地组网
前言
简单而言,Tailscale可以不受限于服务器带宽,使位于不同的网络环境下的设备获得类似于同一局域网下的体验。
Headscale是Tailscale的一个开源服务端,通过go语言完整地支持了绝大多数Tailscale的基础功能,使Tailscale能够完全工作于独立自建的服务端之上。近期恰逢Parsec受到干扰,对未来ZeroTier官方服务在大陆的稳定性有了一些担忧,于是研究了一下自建Headscale服务器的流程。整个过程都是手动安装,在此做一些简单的记录。
原理阐述
同类型工具
- ZeroTier
- Netbird
- frp
- rustdesk
Tailscale服务器端分为两部分,包括负责通信与认证的Headscale服务器和负责打洞和转发的DERP服务器,其工作模式细节可以参考官方博客(点击前往)。以两台计算机为例,它们首先分别通过Tailscale客户端注册至Headscale服务器,在建立通信时先通过Headscale服务器交换握手信息,随后分配到合适的DERP服务器进行中继连接和STUN打洞。若打洞成功,两端将在Headscale服务器引导下绕过服务器建立点对点的直连隧道;若打洞失败,两端将保持通过DERP服务器中继的互联模式。
Tailscale、ZeroTier和Netbird都是功能相似的优秀异地组网工具,且均支持自建服务器。与ZeroTier相比,Tailscale功能更丰富、自建更为简便,同时WireGuard效率更高;缺点是Tailscale客户端资源占用略高(要求RAM>512M)。与Netbird相比,Tailscale起步较早实践资料和可用插件更多(如OpenWRT Luci-UI,点击前往),并且不强制要求独占80与443端口;缺点是WireGuard在go下性能略逊于内核态,同时Headscale并非Netbird一样由官方支持。权衡之后,博主认为Headscale是目前自建比较简单、易用的选择。
打洞工具测试
可以通过NatTypeTester这个工具(点击前往)进行测试能否成功打洞
环境准备
- 系统版本: centos7
- headscale: v0.23.0 (内嵌derper)
- nginx
- certbox
- 一台有公网IP的机器,我这里用阿里云演示
涉及端口
需要在安全组和防火墙进行放行
- tcp 443:对外提供服务
- udp 3478: STUN udp derper端口,负责NAT穿透
准备安装
其中Headscale是一个go的二进制可执行文件,我们直接使用二进制方式安装
headscale安装
官方github地址:https://github.com/juanfont/headscale
参考官方文档: https://headscale.net/setup/requirements/#assumptions
目前最新版本是v0.23.0 ,看官网介绍,本次版本改动比较大,有多个地方进行重构,本次以最新版本进行安装
# 最新版本配置文件example下载地址:
wget -O /usr/local/bin/headscael https://github.com/juanfont/headscale/releases/download/v0.23.0/headscale_0.23.0_linux_amd64
# 有些老系统的 PATH 里没 /usr/local/bin/ ,可以放其他路径里
chmod a+x /usr/local/bin/headscale
mkdir /etc/headscale/
# 下载二进制同版本的示例配置文件
curl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.23.0/config-example.yaml
创建后台启动文件
cat > /etc/systemd/system/headscale.service << EOF
[Unit]
Description=headscale controller
After=syslog.target
After=network.target
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5
# Optional security enhancements
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
WorkingDirectory=/var/lib/headscale
ReadWritePaths=/var/lib/headscale /var/run/headscale
AmbientCapabilities=CAP_NET_BIND_SERVICE
RuntimeDirectory=headscale
[Install]
WantedBy=multi-user.target
EOF
创建用户和目录
因为 headscale 是一个控制中心,不需要特权,我们运行在非 root 用户下,添加用户
useradd \
--create-home \
--home-dir /var/lib/headscale/ \
--system \
--user-group \
--shell /usr/sbin/nologin \
headscale
mkdir -p /var/run/headscale/
# 创建空的 SQLite 数据库文件和 derp 文件:
touch /var/lib/headscale/db.sqlite /etc/headscale/derp.yaml
chown -R headscale:headscale /var/run/headscale/ /var/lib/headscale
chmod a+r /etc/headscale/config.yaml /etc/headscale/derp.yaml
headscale配置文件
接下来 vi /etc/headscale/config.yaml 修改配置文件一些内容:
# grep -v "[[:space:]]#" /etc/headscale/config.yaml |grep -v "^#" |grep -v "^$"
# 配置如下
server_url: https://xxx.domain.com # 对外接受访问的域名,通过反向代理支持ssl证书
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
noise:
# The Noise private key is used to encrypt the
# traffic between headscale and Tailscale clients when
# using the new Noise-based protocol.
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
# v6: fd7a:115c:a1e0::/48
v4: 100.64.0.0/10
# Strategy used for allocation of IPs to nodes, available options:
# - sequential (default): assigns the next free IP from the previous given IP.
# - random: assigns the next free IP from a pseudo-random IP generator (crypto/rand).
allocation: sequential
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: true
# Region ID to use for the embedded DERP server.
# The local DERP prevails if the region ID collides with other region ID coming from
# the regular DERP config.
region_id: 999
# Region code and name are displayed in the Tailscale UI to identify a DERP region
region_code: "headscale"
region_name: "Headscale Embedded DERP"
# Listens over UDP at the configured address for STUN connections - to help with NAT traversal.
# When the embedded DERP server is enabled stun_listen_addr MUST be defined.
#
# For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/
stun_listen_addr: "0.0.0.0:3478"
# Private key used to encrypt the traffic between headscale DERP
# and Tailscale clients.
# The private key file will be autogenerated if it's missing.
#
private_key_path: /var/lib/headscale/derp_server_private.key
# This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically,
# it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths
# If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths
automatically_add_embedded_derp_region: true
# For better connection stability (especially when using an Exit-Node and DNS is not working),
# it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using:
ipv4: x.x.x.x #配置公网IP
#ipv6: 2001:db8::1
# List of externally available DERP maps encoded in JSON
#urls:
# - https://controlplane.tailscale.com/derpmap/default #官方的derper服务站点,基本是国外的,延迟100多ms,建议关闭
# Locally available DERP map files encoded in YAML
#
# This option is mostly interesting for people hosting
# their own DERP servers:
# https://tailscale.com/kb/1118/custom-derp-servers/
#
# paths:
# - /etc/headscale/derp-example.yaml #多个derper服务器,可以通过引入文件方式
paths: []
# If enabled, a worker will be set up to periodically
# refresh the given sources and update the derpmap
# will be set up.
auto_update_enabled: true
# How often should we check for DERP updates?
update_frequency: 24h
disable_check_updates: false
ephemeral_node_inactivity_timeout: 30m
database:
# Database type. Available options: sqlite, postgres
# Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
# All new development, testing and optimisations are done with SQLite in mind.
type: sqlite
# Enable debug mode. This setting requires the log.level to be set to "debug" or "trace".
debug: false
# GORM configuration settings.
gorm:
# Enable prepared statements.
prepare_stmt: true
# Enable parameterized queries.
parameterized_queries: true
# Skip logging "record not found" errors.
skip_err_record_not_found: true
# Threshold for slow queries in milliseconds.
slow_threshold: 1000
# SQLite config
sqlite:
path: /var/lib/headscale/db.sqlite
# Enable WAL mode for SQLite. This is recommended for production environments.
# https://www.sqlite.org/wal.html
write_ahead_log: true
# # Postgres config
# Please note that using Postgres is highly discouraged as it is only supported for legacy reasons.
# See database.type for more information.
# postgres:
# # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank.
# host: localhost
# port: 5432
# name: headscale
# user: foo
# pass: bar
# max_open_conns: 10
# max_idle_conns: 10
# conn_max_idle_time_secs: 3600
# # If other 'sslmode' is required instead of 'require(true)' and 'disabled(false)', set the 'sslmode' you need
# # in the 'ssl' field. Refers to https://www.postgresql.org/docs/current/libpq-ssl.html Table 34.1.
# ssl: false
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""
log:
# Output formatting for logs: text or json
format: text
level: info
policy:
# The mode can be "file" or "database" that defines
# where the ACL policies are stored and read from.
mode: file
# If the mode is set to "file", the path to a
# HuJSON file containing ACL policies.
path: ""
dns:
# Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
# Only works if there is at least a nameserver defined.
magic_dns: flase
# 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: aliyun.xxx.com # 设置为你自己的标识
# List of DNS servers to expose to clients.
nameservers:
global:
- 223.5.5.5 #可以根据自己dns服务器,进行修改
- 223.6.6.6
- 2606:4700:4700::1111
- 2606:4700:4700::1001
# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
# "abc123" is example NextDNS ID, replace with yours.
# - https://dns.nextdns.io/abc123
# Split DNS (see https://tailscale.com/kb/1054/dns/),
# a map of domains and which DNS server to use for each.
split:
{}
# foo.bar.com:
# - 1.1.1.1
# darp.headscale.net:
# - 1.1.1.1
# - 8.8.8.8
# Set custom DNS search domains. With MagicDNS enabled,
# your tailnet base_domain is always the first search domain.
search_domains: []
# Extra DNS records
# so far only A-records are supported (on the tailscale side)
# See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
extra_records: []
# - name: "grafana.myvpn.example.com"
# type: "A"
# value: "100.64.0.3"
#
# # you can also put it in one line
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
# DEPRECATED
# Use the username as part of the DNS name for nodes, with this option enabled:
# node1.username.example.com
# while when this is disabled:
# node1.example.com
# This is a legacy option as Headscale has have this wrongly implemented
# while in upstream Tailscale, the username is not included.
#use_username_in_magic_dns: false
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
# Enable logtail for this headscales clients.
# As there is currently no support for overriding the log server in headscale, this is
# disabled by default. Enabling this will make your clients send logs to Tailscale Inc.
enabled: false
randomize_client_port: true #配置改成true,支持多个端口
headscale 启动
# 测试文件
headscale configtest
# 配置文件没问题就使用 systemd 启动
chown -R headscale:headscale /var/lib/headscale
systemctl daemon-reload
systemctl enable --now headscale
derper
headscale已经内嵌derper,在上面配置中已经启用了内嵌derper,最后我们看下运行效果
nginx反向代理
上一篇文章,我们介绍了SSL的证书自动续签,不清楚的,可以看下上一篇文章,本次我们采用certbot 和 nginx做反向代理
nginx 要支持反向代理http和websocket 俩个协议哈 (这个配置很重要,不懂得不要自己随意更改,直接copy)
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name xxx.domain.com;
access_log /data/nginx/logs/access.log;
client_header_timeout 1200s;
client_body_timeout 1200s;
client_max_body_size 500m;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
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;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
proxy_pass http://127.0.0.1:8080;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/xxx.domain.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/xxx.domain.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = xxx.doamin.com) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name xxx.domain.com;
return 404; # managed by Certbot
}
加载nginx ,使配置生效
systemctl reload nginx
访问下面俩个地址,如果能正常打开,代表配置成功 下面俩个地址会提示俩种终端的接入提示文档,windows接入我们在后面会讲
https://xxx.domain.com/windows 和https://xxx.domain.com/apple
客户端接入
需要把 ecs 的公网 IP 设置成直连不走代理,以及如果是家里宽带,可以把 Upnp 打开,这样打洞直连成功率会高些。
创建authkeys
Tailscale 中有一个概念叫 tailnet,你可以理解成租户,租户与租户之间是相互隔离的,具体看参考 Tailscale 的官方文档: What is a tailnet。 Headscale 也有类似的实现叫 user,即用户。我们需要先创建一个 user,以便后续客户端接入,例如:
headscale user create default
# 生成一个过期时间 365d 且可以重复使用的 authkey
$ headscale preauthkeys --user default create --reusable --expiration 365d
# 查看已经生成keys
[root@bdser ~]# headscale preauthkeys --user default list
ID | Key | Reusable | Ephemeral | Used | Expiration | Created | Tags
1 | 8616f01720ea4c219b2b2................ | true | false | true | 2025-11-05 17:08:02 | 2024-11-05 17:08:02 |
客户端tailscale介绍
tailscale 也是分为 tailscaled 的 daemon 和 tailscale 的 cli 工具,windows、Linux 以及安卓的 Magisk 模块等都可以使用 cli 工具操作和排查,这点很重要。
下面是 tailscale up 时候一些常用通用选项:
- –login-server: 指定使用的中央服务器地址(必填)
- –advertise-routes: 向中央服务器报告当前客户端处于哪个内网网段下, 便于中央服务器让同内网设备直接内网直连(可选的)或者将其他设备指定流量路由到当前内网(可选),多条路由英文逗号隔开
- –accept-routes: 是否接受中央服务器下发的用于路由到其他客户端内网的路由规则(可选)
- –accept-dns: 是否使用中央服务器下发的 DNS 相关配置(可选, 推荐关闭)
- –hostname: 设置 machine name,否则默认会以 hostname 注册上去,特别安卓的 hostname 无法修改 tailscale cli 官方文档 https://tailscale.com/kb/1080/cli , 也可以自己 tailscale –help 看命令帮助。
linux接入
linux本机接入,也就是安装headscale服务端这台机器,hostname一定要提前想好,一旦注册,不知道怎么修改。
Download地址: https://tailscale.com/download/linux
#直接一键安装
curl -fsSL https://tailscale.com/install.sh | sh
#客户端发起登录注册,这个很重要 ,最后刚刚生成authkey
tailscale up --login-server=https://xxx.domain.com --accept-routes=true --hostname ecs-aliyum --accept-dns=false --authkey 3333....
windows接入
windows下载地址: https://tailscale.com/download/windows
windows安装包下载好,一路点点点安装
安装好tailscale后,右下角logo图标点击,如果之前有login登录,可以先退出,或者选择Add annother count,然后在终端命令行,通过cli注册
# 通过打开powershell终端,进入安装tailscale目录的文件夹,里面有tailscale二进制文件可执行
# tailscale --help 看命令帮助
# 客户端发起登录注册
tailscale up --login-server=https://xxx.domain.com --accept-routes=true --hostname bdser-windows --accept-dns=false --authkey 3333....
查看注册节点
[root@bdser ~]# headscale nodes list
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | laptop | laptop | [A2o6t] | [JLp63] | default | 100.64.0.1, | false | 2024-11-06 03:08:07 | 0001-01-01 00:00:00 | online | no
2 | ecs-aliyun | ecs-aliyun | [jn3a7] | [rJB9J] | default | 100.64.0.3, | false | 2024-11-06 03:02:39 | 0001-01-01 00:00:00 | online | no
3 | office-windows | office-windows | [3WiZk] | [v1YSb] | default | 100.64.0.4, | false | 2024-11-06 10:37:03 | 0001-01-01 00:00:00 | online | no
查看网络情况
查看derper情况
[root@bdser ~]# tailscale netcheck
Report:
* UDP: true
* IPv4: yes, 47.98.179.216:41925
* IPv6: no, but OS has support
* MappingVariesByDestIP:
* PortMapping:
* Nearest DERP: Headscale Embedded DERP
* DERP latency:
- headscale: 4.4ms (Headscale Embedded DERP)
查看status
[root@bdser ~]# tailscale status
100.x.x.x ecs-aliyun default linux -
100.x.x.x laptop default windows -
100.x.x.x office-windows default windows -
查看ping 这里先是通过derper建立连接,如果一直打洞失败,就一直走derper中继
[root@bdser ~]# tailscale ping office-windows
pong from office-windows (100.64.0.4) via DERP(headscale) in 21ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 20ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 20ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 23ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 22ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 20ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 21ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 19ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 23ms
pong from office-windows (100.64.0.4) via DERP(headscale) in 21ms
direct connection not established
我把公司和家里的俩个windows都接入了headscale中,看下ping延迟情况
大概延迟在40ms内,看起来网速还是可以的,使用一段时间,看看稳定性如何把~
PS C:\Users\dongshu.bu> tailscale.exe ping laptop
pong from laptop (100.64.0.1) via DERP(headscale) in 38ms
pong from laptop (100.64.0.1) via DERP(headscale) in 37ms
pong from laptop (100.64.0.1) via DERP(headscale) in 35ms
pong from laptop (100.64.0.1) via DERP(headscale) in 34ms
pong from laptop (100.64.0.1) via DERP(headscale) in 33ms
pong from laptop (100.64.0.1) via DERP(headscale) in 34ms
pong from laptop (100.64.0.1) via DERP(headscale) in 34ms
修改节点信息
修改注册名称
tailscale set --hostname=xxx
重启tailscale ,使之生效
tailscale down
tailscale up
打通内网
Linux 端都要开启转发,windows 和安卓转发自行查找怎么配置。
echo 'net.ipv4.ip_forward = 1' | tee /etc/sysctl.d/ipforwarding.conf
echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/ipforwarding.conf
sysctl -p /etc/sysctl.d/ipforwarding.conf
ID | Hostname | Name | MachineKey | NodeKey | User | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | laptop | laptop | [A2o6t] | [JLp63] | default | 100.64.0.1, | false | 2024-11-06 03:08:07 | 0001-01-01 00:00:00 | online | no
2 | ecs-aliyun | ecs-aliyun | [jn3a7] | [rJB9J] | default | 100.64.0.3, | false | 2024-11-06 03:02:39 | 0001-01-01 00:00:00 | online | no
3 | office-windows | office-windows | [3WiZk] | [v1YSb] | default | 100.64.0.4, | false | 2024-11-06 10:37:03 | 0001-01-01 00:00:00 | online | no
设 ID==1 的局域网是 192.168.31.0/24 网段,我们希望其他 ID 设备上能访问到,先查看路由:
headscale routes list
headscale routes enable -r 1
ip route show table 52 | grep "192.168.31.0/24"
其他节点启动时需要增加 –accept-routes=true 选项来声明 “我接受外部其他节点发布的路由”。
现在你在任何一个 Tailscale 客户端所在的节点都可以 ping 通家庭内网的机器了,你在公司或者星巴克也可以像在家里一样用同样的 IP 随意访问家中的任何一个设备。
一个正在运行的节点增加路由可以使用 set 命令:
# 多条用英文逗号间隔
tailscale set --advertise-routes xx.xx.xx.0/24,xx,xxx.xxx.00.00/16
总结
这里只介绍异地组网部分,其他的去看官方文档。
补充
阿里云机器dns和yum源问题, 由于阿里云这些服务也用的是100网段,会冲突
第一种解决办法
yum源更改
sed -i 's/http:\/\/mirrors\.cloud\.aliyuncs\.com/https:\/\/mirrors\.aliyun\.com/g' *.repo
dns更改成114.114.114.114
[root@bdser yum.repos.d]# cat /etc/resolv.conf
# Generated by NetworkManager
nameserver 114.114.114.114
第二种解决办法
把100网段的DROP的策略,通过iptables -D 删除掉
[root@bdser yum.repos.d]# iptables -S |grep DROP |grep 100
# Warning: iptables-legacy tables present, use iptables-legacy to see them
-A ts-input -s 100.64.0.0/10 ! -i tailscale0 -j DROP
-A ts-forward -s 100.64.0.0/10 -o tailscale0 -j DROP