自建 Tailscale 服务并实现多设备组网互联

1. 简介

Tailscale 是基于 wireguard 的跨平台虚拟组网工具,可以非常方便地实现多个设备的 mesh 虚拟组网。组网后,设备间可以直接使用内网 IP / 域名进行通信,可以随意切换网络,不会引起地址变动。这在远程访问、自建服务等场景下是极为有用的。

举例而言,接入统一的 Tailscale 网络后,你可以在外随时随地访问家中的 NAS 设备而无需公网 IP 或 DDNS 服务,同时享受到 NAT 穿透带来的几乎满带宽的网速体验;你可以随处使用手机 / 平板通过 moonlight 串流家中的电脑进行 “云游戏 “,将强大的计算与渲染性能随身携带;对于基友小规模自建服务器联机游戏的场景,Tailscale 更是适合不过。

另外,Tailscale 还提供了不少非常实用的功能,如文件隔空投送 Taildrop,将其他网段设备接入 Tailscale 的子网路由功能(可以远程共享 NAS、打印机),内网 MagicDNS 模块,出口路由功能等等。可以说极为强大。

唯一的缺点可能是,Tailscale 是国外服务,借用国外现成的 oauth 系统(比如谷歌账号等)进行账户管理,这在国内使用起来相对不是很方便。而且在国内,并没有现成的 derper 中继服务器,直接使用 Tailscale 默认的服务器会出现延迟爆炸的问题。

我们可以通过在国内的服务器上自行搭建开源的控制服务端 Headscale,配合自建官方开源的 derper 中继服务器,来彻底解决这个问题。

2. 大致通信原理

处于同一个 Telnet 内的设备之间将要进行通信时,Tailscale 会自动尝试 NAT 打洞进行 P2P 直连,穿透后设备等同于直连,设备间通信延迟极低。Tailscale 团队对 NAT 穿透有较为深入的研究,其穿透技术对各种类型的 NAT 的都有相当不错的效果。

如果打洞无法成功,Tailscale 将通过 Derp 中继协议进行端到端加密的 TCP 中继,自动根据延迟等因素选用最优的中继服务器进行连接。TCP 中继有效解决了 wireguard UDP 公网大流量被 QOS 的问题。

另外,Tailscale 通过 SDN 的设计,支持一键授权新节点并自动下发配置文件,解决了 wireguard 网络增删设备时需要手动配置所有节点,灵活性不够好的问题,并且还提供端到端加密通信和 ACL 控制等安全机制。

3. 服务端

自建 Tailscale 需要搭建两个服务端:控制平面 Headscale 和中继服务器 Derper。

3.1. Headscale 部署

这里使用 docker 部署,caddy 反代并自动申请证书,另外还部署了开源项目 Headscale Admin 面板,用来进行简单的服务端控制。

docker-compose 文件如下,这里使用了 Traefik 作为反代服务器,其配置非常适应 docker 环境下的部署:

version: '3'

services:
  headscale:
    container_name: headscale
    image: headscale/headscale:latest
    restart: unless-stopped
    volumes:
      - ./headscale-config:/etc/headscale
      - ./headscale-data:/var/lib/headscale
      - ./headscale-run:/var/run/headscale
    entrypoint: headscale serve
    networks:
      - traefik
    labels:
      traefik.enable: true
      traefik.http.routers.headscale.tls: true
      traefik.http.routers.headscale.tls.certresolver: letsencrypt
      traefik.http.routers.headscale.entrypoints: https
      traefik.http.routers.headscale.rule: Host(`<Headscale服务器域名>`)
      traefik.http.services.headscale.loadbalancer.server.port: 8080

  # admin panel
  headscale-admin:
    image: goodieshq/headscale-admin:latest
    container_name: headscale-admin
    restart: unless-stopped
    user: "1003"
    depends_on:
      - headscale
    networks:
      - traefik
    labels:
      traefik.enable: true
      traefik.http.routers.headscale-admin.tls: true
      traefik.http.routers.headscale-admin.tls.certresolver: letsencrypt
      traefik.http.routers.headscale-admin.entrypoints: https
      traefik.http.routers.headscale-admin.rule: Host(`<Headscale服务器域名>`) && PathPrefix(`/admin`)
      traefik.http.services.headscale-admin.loadbalancer.server.port: 80

除了 docker-compose 文件之外,还需要将 headscale 项目的 config-example.yaml 复制到./headscale-config 目录下,改名 config.yaml。对下面这些项要做修改:

...
server_url: https://<Headscale服务器域名>:<工作端口>
...
prefixes:
  v4: 100.64.0.0/10
  # v6: fd7a:115c:a1e0::/48		# 把IPv6注释掉,按需
...
derp:
  server:
    enabled: false				# 禁用内置derper服务器(感觉直接用官方的会更好)
...

全部配置完成之后,启动容器,Headscale 即可在 https://<Headscale服务器域名>:<工作端口> 访问到。记得要在 Traefik 中配置泛域名 HTTPS 证书申请

3.1.1. 配置管理面板

前面的 docker-compose 中已经部署了 goodieshq/headscale-admin 管理面板的镜像,可以通过 https://<Headscale服务器域名>:<工作端口>/admin 地址访问到。通过管理面板可以快速添加、管理与删除节点、修改配置,不用再每次上服务器跑命令行。

管理面板需要 Headscale 的 API Key 以访问其接口。在 docker 容器部署完毕开始运行后,在服务器执行如下命令:

docker exec headscale headscale apikey create

这样会创建一个新的 Headscale API Key。将此 Key 填入管理面板的 Settings 页面并保存,即可开始使用管理面板。

3.2. Derper 中继部署

Derper 中继服务器的部署仍然使用 docker,采取 tailscale 官方的镜像搭建。在环境变量配置中打开 verify-client 验证客户端,防止自己的 Derper 中继服务器被别人白嫖。

3.2.1. 生成证书

按照官方文档,Derper 使用 TLS 进行通信,但其对标准的 TLS 行为进行了一定的修改,理论上不适合用反代进行代理,因此我们直接将它的服务端口对外暴露。

由于不能使用反代,Traefik 申请的证书又没法直接为 Derper 所用(格式不对),这里按之前 docker-compose 使用 certbot 经验记录 帖子的经验,增加一个 certbot 容器一起运行,每次自动申请 Derper 服务器域名所对应的 HTTPS 证书,并导出到./cert 目录下供 Derper 使用。

具体操作详见上述帖子,这里不再赘述。

事实上,最好的方案是生成一个长期有效的自签名证书给 Derper 使用,这样既不需要反复续期,也可以正常达到流量加密的效果。但是目前 headscale 这边仍未解决客户端需要证书验证的问题,此方案有待后续研究

3.2.2. 部署 Derper 服务器

Derper 服务器的 docker-compose 文件如下:

derper:
  container_name: headscale-derper
  image: fredliang/derper
  restart: unless-stopped
  depends_on:
    - headscale
  ports:
    - 12344:12344/udp
    - 12345:12345
  environment:
    DERP_DOMAIN: "<Derper服务器域名>"
    DERP_CERT_MODE: "manual"
    DERP_CERT_DIR: "/cert"
    DERP_ADDR: ":<Derper工作端口>"
    DERP_STUN_PORT: "<STUN工作端口>"
    DERP_HTTP_PORT: "-1"
    DERP_VERIFY_CLIENTS: "false"
    DERP_VERIFY_CLIENT_URL: "https://<Headscale服务器域名>:<工作端口>/verify"
  volumes:
    - "./cert:/cert"

其中 12344 是 stun 的端口,12345 是 TCP 中继工作的端口。这里因为不使用反代,因此不用接入 Traefik。证书模式为 manual,使用前面 certbot 自动申请的 Let’s Encrypt 证书。

3.2.3. 防止 Derper 被白嫖

部署完成后,为了不使自己的 derper 服务器被别人白嫖,需要启用客户端认证。(版本注意)所有 v0.23.1 及以后的 Headscale 版本(含有 这个 commit),均提供了一个 /verify 接口用于进行客户端身份验证。相比于传统的 Tailscale 客户端验证,这个方法要简单方便得多,具体的配置方法如下:

  • 在 docker-compose 中设置 DERP_VERIFY_CLIENTS: "false"
  • DERP_VERIFY_CLIENT_URL 设置为 https://<Headscale服务器域名>:<工作端口>/verify

这样,每当有客户端试图连接到 Derper 时,它会通过 Headscale 的接口查询验证客户端身份,进而决定接收 / 拒绝请求,有效防止被其它未知客户端白嫖。

3.2.4. 将服务器写入 derp map

derper 部署完成之后,需要修改 headscale 的 derp-map 配置,将此 derper 节点加入中继服务器列表中。

./headscale-config/config.yaml 中,设置:

derp:
...
  paths:
    - /etc/headscale/derp.yaml

然后在./headscale-config/derp.yaml 中添加 derp node 条目,按实际情况修改以下配置中的各项参数:

regions:
  900:
    regionid: 900
    regioncode: <区域缩写(两个字母)>
    regionname: <区域全名>
    nodes:
      - name: 900a
        regionid: 900
        hostname: <Derper服务器域名>
        ipv4: <Derper服务器IPv4地址>
        # ipv6: <Derper服务器IPv6地址>
        derpport: <Derper工作端口>
        stunport: <STUN工作端口>
        stunonly: false		# 是否仅把此node当作stun服务器使用

其中区域名称、区域编号等可以按需设置。注意,Derper 服务器本身以 TCP 协议工作,而其内置的 STUN 服务器则以 UDP 协议工作,在防火墙放通端口的时候需要注意。

全部设置完毕后,启动容器即可。接下来使用浏览器打开 derper 服务器的工作端口,观察其是否有正常的 HTTP response,如果能访问到表明部署成功。

最后,需要设定一个 crontab 任务,定时重启 certbot 和 derper 两个容器,以自动续期证书。

4. 客户端接入

4.1. Windows

直接安装官方客户端,按通过命令行登录自定义 headscale 服务器即可

tailscale up --login-server=https://<Headscale服务器域名>:<工作端口> --unattended

4.2. 安卓

正常情况下,可以使用官方客户端,其 UI 十分美观好用。

在国内的网络环境下,需要对梯子进行兼容,详见下面” 梯子兼容问题 “

5. 梯子兼容问题

国内普遍网络环境问题,导致平时经常需要使用梯子。tailscale 与梯子应用之间存在一些协调性问题,比如安卓平台只能同时运行一个 VPN 应用,导致 tailscale 应用和梯子 APP 无法共存,只能同时开启一个;另外,Windows 平台上梯子启用 TUN 模式时,若不对路由等进行额外处理,tailscale 也无法工作。

5.1. 修改梯子配置

要使 Tailscale 和梯子互不冲突,可以通过设定梯子的规则和 TUN 绕过 Tailscale 网段工作,以避免错误地路由其流量。以 Clash.Meta 为例,如下设置配置文件:

tun:
  ...
  route-exclude-address:	# 绕过tailscale网段
    - "100.64.0.0/10"
...
hosts:
  # 可以在这里加Tailscale内网DNS解析条目,以代替MagicDNS的功能
...
dns:
  ...
  use-hosts: true
  fake-ip-filter:       # fake ip 白名单列表
    - "<Headscale服务器域名>"
    ...
...
rules:
  # 不路由Tailscale流量
  - IP-CIDR,100.64.0.0/10,DIRECT,no-resolve
  - DOMAIN-SUFFIX,<Headscale服务器域名>,DIRECT,no-resolve
  ...

5.2. 安卓平台:(方案一)改用 tailscale-magisk 模块,梯子随意

在安卓平台上,如果你的设备已经 root,可以使用 Magisk-Tailscaled 模块来实现在安卓设备上运行 linux 版 tailscale。安装模块后,直接在有 root 权限的终端执行 tailscale 命令使用即可。

由于项目尚未针对安卓平台进行 tailscaled 的交叉编译,因此只能以 userspace 模式工作,而且存在一些 BUG,比如 MagicDNS 不支持等,还有一些功能比如 Taildrop 等也无法使用,较为遗憾。但是关于虚拟组网互联的所有必要功能都是可以正常工作的。

5.3. 安卓平台:(方案二)梯子改用 box4magisk,Tailscale 仍使用 APP

同样地,可以将梯子改为使用模块,Tailscale 仍然使用 APP,这种方式更为推荐,可以充分发挥 Tailscale APP 的全部功能。

TODO:详解

6. 内网 DNS 配置

Tailscale 官方提供了 MagicDNS 功能,在 admin 面板自定义各个主机名后,就可以通过 MagicDNS 用自定义的主机域名来访问到各主机。

不过事实上,考虑到和梯子的协同问题,为了避免出现 BUG,目前各设备上均暂不启用 MagicDNS。有两种方法实现类似 MagicDNS 的效果:

  1. 在梯子配置文件的 hosts 段中,针对各主机手动设置 hosts 条目
  2. 在 headscale 域名所在服务商处(如 Cloudflare)手动添加对应主机的 DNS 条目

这些都可以达到类似的效果。

7. 补:zerotier 和 tailscale 的区别

粗略来看,两者都是虚拟组网方案,在性能上也区别不大。不过在技术实现细节上有一定的差异:

7.1. 网络底层实现

  • zerotier 是二层(数据链路层)虚拟组网方案,更贴近理解上的 “虚拟局域网” 的概念,对于大部分协议都有比较好的支持
  • tailscale 是基于 wireguard 的三层(网络层)点对点虚拟组网方案,其主要支持三层协议的点对点通信。某些需要二层支持的协议 Tailscale 无法使用。比如,截至目前(2024.10)并不支持 UDP 广播、mDNS、Bonjour 等,导致一些设备互联 / 游戏等场景下的的局域网设备发现无法正常工作。

7.2. 通信协议实现

  • zerotier 通过 TCP 协议直连通信。自建 moon 服务器进行中继时默认使用 UDP 通信,不过可以在客户端手动切换到 TCP 中继模式
  • tailscale 基于 wireguard,直连时使用 UDP,高峰期或者大流量下可能受到运营商 QoS。自建 derp 服务器中继则是使用 TCP 进行中继通信,相对较为稳定。

7.3. 附属生态与功能

  • tailscale 有非常丰富的附属功能,如文件投送、子网路由、出口路由、自定义 DNS 等等。
  • zerotier 专注于实现虚拟组网,附属功能较少。

另外,似乎有评价说相较于 Zerotier,Tailscale 的易用性、UI 美观度等各方面都要更强一些。这个仁者见仁智者见智吧,有需求的话可以亲自测试,感受一下两者的区别。


自建 Tailscale 服务并实现多设备组网互联
https://blog.openyq.top/posts/64921/
作者
yqs112358
许可协议