..

Tailscale 组网实践

引子

自从家里 HomeLab 跑的服务越来越多,我也越来越离不开这个网络环境。赶在过年前,好好研究了组网。过程相对还是轻松的,只是流程多,写篇博客做记录。

组网和内网穿透

组网,即把不同区域的设备,组到一个虚拟局域网中,在这个虚拟局域网内,设备之间可以互相访问。组网和内网穿透很像,但又不大一样,内网穿透是把内网服务器中的某个服务暴露出去供外网访问,而组网是把整个内网服务器暴露出去。在之前,我一直用 Cloudflare Tunnel 实现内网穿透,也很简单,服务器起一个 cloudflared 服务,再在 Cloudflare 后台添加端口映射规则就好。但是这么做有几个麻烦:

  1. 众所周知,Cloudflare 在国内是个减速器,别说跑满家里上传速度,能稳定访问就很好了;
  2. 我通常将内网的服务绑定到局域网域名下。比如内网搭建了一个 FreshRSS 服务,域名为 freshrss.xx.com,但你几乎不可能买到这么简短好记的域名,穿透到公网域名可能是 freshrss.yyyyyy.com,这很麻烦,域名不同会带来很多不必要的负担;
  3. 使用 Cloudflare Tunnel 实现穿透有局限性。比如我经常使用 Sunshine + Moonlight 连接到局域网 Windows 游戏机打游戏,这个场景就无法使用穿透实现外网串流,我没找到解决方式;
  4. 最后是安全问题。内网服务通常使用弱口令,有些甚至没有鉴权,这些服务暴露到公网都要再套一层 nginx 用于鉴权,麻烦不说,还要多记一个密码。

目前,组网方案已经相当多了,这里就不赘述了,我相信很难有比 Tailscale 更加易用简单的了,它基于 WireGuard 做底层协议,也足够安全,关键是用作个人用途还免费,这还要啥自行车。

网络环境介绍

让我先简单介绍一下目前家里的网络环境,这里省略一些不必要设备,只大概介绍基本结构。我使用运营商的光猫拨号,连到路由器,路由器下面连接交换机,挂载一台 Debian 服务器、一台 Windows 游戏机及一些其它设备。

Figure 1: 网络环境示意图

Figure 1: 网络环境示意图

我个人倾向将不同需求和功能通过硬件分离,方便升级设备。比如路由器就只负责提供 Wi-Fi,服务器只负责跑服务,设备之间互相不耦合,这样即使某个设备出了故障也可以快速恢复,后续升级也更方便些。

目前,我的 HomeLab 服务器大概跑了四十个服务,几乎所有服务都是绑定到同一个局域网域名下,通过二级域名区分(比如 freshrss.xx.com、nextcloud.xx.com),配合自签证书实现 HTTPS 访问。服务器上还有一个 DNS 服务,实现局域网域名绑定。在需要使用内网服务的设备上,手动安装 HTTPS 证书,设置 DNS,便可以正常访问内网服务。

所以,我的组网需求就是,当我不在家里时,可以访问家里内网 192.168.x.x 网段,并且同时也可以访问这个局域网域名和这台局域网服务器的所有资源。

Tailscale 部署

局域网服务器节点配置

首先,在局域网服务器上,通过 Docker Compose 运行 Tailscale 服务,我的 docker-compose.yml 如下:

services:
  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    hostname: debian_server
    environment:
      - TS_STATE_DIR=/var/lib/tailscale
      - TS_ACCEPT_DNS=true
      - TS_USERSPACE=0
      - TS_ROUTES=192.168.1.0/24
      - "TS_EXTRA_ARGS=--accept-routes --advertise-exit-node"
    volumes:
      - /dev/net/tun:/dev/net/tun
      - ./data/tailscale/state:/var/lib/tailscale
      - ./data/tailscale/tmp:/tmp
    cap_add:
      - net_admin
      - sys_module
    restart: unless-stopped
    network_mode: host

(如果你无法访问下载 docker 镜像,可以使用南京大学镜像,将 tailscale/tailscale:latest 更换为 ghcr.nju.edu.cn/tailscale/tailscale:latest

其中,

hostname 是主机名,按你实际情况填写。

TS_ROUTES=192.168.1.0/24 标识用于暴露 192.168.1.x 子网网段,如果你想暴露多个网段,可以像这样 ROUTES=192.168.1.0/24,192.168.2.0/24 一块添加进去。暴露子网网段的好处是,组网后可以直接访问整个网段的设备。比如你可以通过 192.168.1.1 直接访问路由器后台,即使你无法在路由器上安装 Tailscale 客户端。

TS_EXTRA_ARGS=--accept-routes --advertise-exit-node 是运行参数, --accept-routes 是用于暴露子网网段, --advertise-exit-node 是用于开启出口节点。出口节点(Exit Node)用于转发流量,通俗的描述就是,开启出口节点并启用,组网后,你通过手机网络访问百度,实际是你通过你的出口节点服务器访问百度,如果没有需求的话,可以直接去掉。需要注意的是目前只支持将 Linux 机器作为出口节点。

更多参数见官方文档

启动服务后,使用 docker compose logs 查看日志,找到 https://login.tailscale.com/a/xxxxxxxxxxxxxx 样式的链接,通过浏览器访问完成授权添加节点。

完成授权后,打开 Tailscale 网页后台,在设备列表找到对应设备,点击设备右侧的三个点,选择「Edit route settings…」,勾选对应的子网网段,如果你上面启用了出口节点,记得一并勾选下面的「Use as exit node」,之后点击保存以应用更改。

Figure 2: 设备路由配置图

Figure 2: 设备路由配置图

这里我还禁用了密钥过期,即在 Tailscale 对应设备菜单选择「Disable key expiry」,这样之后可以通过备份 docker-compose.yml./data/tailscale/state/tailscaled.state./data/tailscale/state/tailscaled.log.conf 快速迁移。

手机节点配置

使用手机下载 Tailscale 客户端(iOS 用户需要换区才可以下载)。打开 Tailscale 客户端并登录,这时关闭 Wi-Fi 网络,切换到流量网络,打开 Tailscale 组网开关,通过浏览器访问路由器后台试试看,如果可以正常访问,说明已经组网成功。如果出现问题,可以登录 Tailscale 后台看看局域网服务器是否在线。

之后,你想用于组网的设备都只需要安装 Tailscale 客户端并登录即可。

Tailscale DNS 配置

接下来处理局域网域名的问题。打开 Tailscale 后台,切换到「DNS」标签页。我在局域网服务器上使用 adguardhome 部署了一个 DNS 服务器,所以这里只需要通过局域网 DNS 服务器解析局域网域名即可。(这里需要注意的是,如果你像我一样在局域网服务器使用 Docker 部署 Tailscale 节点,那请务必确保 network_modehost ,或者确保其和 DNS 服务器在同一个 Docker 网络下)

Tailscale 提供了 Split DNS 的功能,可以很方便地指定用于特定域名的 DNS 服务器,像下图就是使用 192.168.1.33 这个 DNS 服务器来解析 test.com 。这里按你的实际情况填写 Nameserver 和 Domain 即可。

Figure 3: 局域网域名解析配置图

Figure 3: 局域网域名解析配置图

保存后,在手机上切换到流量网络,重启 Tailscale,打开浏览器访问内网域名,会发现可以直接访问啦。

Derper 中转服务器部署

完成上面的部署后,已经算是基本可用。但你会发现网页加载特别慢,这是因为 Tailscale 在国内没有中继节点,如果无法打洞实现直连的话,速度会很慢。

Figure 4: 拨号上网级网速

Figure 4: 拨号上网级网速

我们可以通过在一台有公网 IP 的服务器上部署 DERP 实现中继。组网的速度上限取决于你家里的宽带最大上传速度。我使用的联通宽带,上传速度在 25Mbps 左右,买了一台四十块一个月峰值 200Mbps 的阿里云轻量服务器作为中继服务(想要好速度没得选,这已经是我找到的最便宜的了,要不然就只能用那种几兆的小水管了,体验不会好的🫠)。

部署主要参考了 【Tailscale】docker compose部署基于IP的Derp节点并提供鉴权,大佬写得很好,这里基本是复制过来。

使用 Docker Compose 部署, docker-compose.yml 文件如下:

services:
  derper:
    image: ghcr.io/yangchuansheng/ip_derper:latest
    container_name: derper
    restart: always
    volumes:
      - /usr/share/zoneinfo/Asia/Shanghai:/etc/localtime:ro
      - ./data/tailscale/tmp:/var/run/tailscale
    ports:
      - "10043:443"
      - "10078:3478/udp"
    environment:
      DERP_VERIFY_CLIENTS: "true" #使用tailscale客户端鉴权
    depends_on:
      - tailscale

  tailscale:
    image: tailscale/tailscale:latest
    container_name: tailscale
    restart: always
    hostname: XXX
    network_mode: host  #注释本行可使VPS主机不可通过tailscale网络访问
    environment:
      TS_STATE_DIR: /var/lib/tailscale
      TS_USERSPACE: false
    volumes:
      - /dev/net/tun:/dev/net/tun
      - ./data/tailscale/state:/var/lib/tailscale
      - ./data/tailscale/tmp:/tmp
    cap_add:
      - net_admin
      - sys_module

(如果你无法访问下载 docker 镜像,可以使用南京大学镜像,将 tailscale/tailscale:latest 更换为 ghcr.nju.edu.cn/tailscale/tailscale:latest ;将 ghcr.io/yangchuansheng/ip_derper:latest 更换为 ghcr.nju.edu.cn/yangchuansheng/ip_derper:latest 。)

打开 Tailscale 网页后台,切换到「Access controls」标签页,添加如下配置:

"derpMap": {
        "OmitDefaultRegions": false, // 设置为 true不下发官方derp节点
        "Regions": {
            "900": {
                "RegionID":   900,
                "RegionCode": "XXX",
                "RegionName": "XXX_Beijing",
                "Nodes": [
                    {
                        "Name":             "900",
                        "RegionID":         900,
                        "HostName":         "0.0.0.0", // 你的公网IP地址
                        "IPv4":             "0.0.0.0", // 你的公网IP地址
                        "DERPPort":         10043,
                        "InsecureForTests": true, // 客户端不做校验
                        "STUNPort":         10078,
                    },
                ],
            },
        },
    },

RegionID 随便填,只要是 9XX 就可以,有多个节点的话记得使用不同的 ID。 RegionCodeRegionName 也是随便填,这是方便你在后续网络检查中查看的。

容器启动后使用 docker logs tailscale 查看日志,找到 https://login.tailscale.com/a/xxxxxxxxxxxxxx 样式的链接,同样是用浏览器完成授权添加节点。

节点添加后可以在 Tailscale 对应设备菜单选择「Disable key expiry」,这样备份 docker-compose.yml./data/tailscale/state/tailscaled.state./data/tailscale/state/tailscaled.log.conf 后就可以快速迁移。

中继节点部署好后,可以通过局域网设备运行 docker exec tailscale tailscale netcheck 命令查看是否生效以及节点延迟:

$ docker exec tailscale tailscale netcheck
Report:
    * Time: 2025-01-22T08:19:42.936928293Z
    * UDP: true
    * IPv4: yes, 1.1.1.1:111
    * IPv6: no, but OS has support
    * MappingVariesByDestIP:
    * PortMapping:
    * Nearest DERP: XXX_Beijing
    * DERP latency:
        - XXX: 12.2ms  (XXX_Beijing)

此时通过手机使用流量网络访问局域网网页,会发现流畅不少,基本可以跑满上传带宽了。

Figure 5: 启用中继节点后

Figure 5: 启用中继节点后

甚至可以串流到局域网的 Windows 游戏机打游戏了。

Figure 6: 使用 Moonlight 串流游戏

Figure 6: 使用 Moonlight 串流游戏

访问控制

经过上面的折腾后,基本已经可以流畅使用了。但是,可能会有一些安全问题。

通常,内网的服务和公网的服务在安全性上天差地别。在内网服务,为了方便我们可能使用弱口令,甚至不做任何鉴权。而假如有人真的黑进了你的中转服务器,或是你将其它不属于你的设备加入了这个虚拟局域网,内网服务器上的数据就可能被人窃取。

Tailscale 默认情况下允许所有设备互相访问,但我们可以自己写规则,灵活地调整局域网设备的访问规则。

这里我以我自己的实际情况举例。我根据设备按属性分了四类标签,分别是:

  • user: 普通用户,可以访问局域网网段所有资源;
  • guest: 访客,只能访问局域网的网页;
  • server: 服务器,不能访问任何设备;
  • derper: 中继服务器,不能访问任何设备。

打开 Tailscale 后台,切换到「Access controls」标签页,添加如下标签定义:

"tagOwners": {
    "tag:user":   ["[email protected]"],
    "tag:guest":  ["[email protected]"],
    "tag:server": ["[email protected]"],
    "tag:derper": ["[email protected]"],
},

这里的 [email protected] 为自己的账号。这串定义用于描述标签所有者,说起来比较绕口,简单说就是谁可以打标签,比如上述代码就是 [email protected] 这个用户可以给设备打 user、guest、server、derper 标签。

注意,打标签权限是一个危险权限。拥有该权限的人可以随意更改设备标签来绕过访问限制。如果你的 Tailscale 还有其它用户,请尽量只让一个人负责打标签,不要随意给其它用户分配该权限。

接着,对不同的设备打标签。切换到「Machines」标签页,点击对应设备右侧的三个点,选择「Edit ACL tags…」,再点击「Add tags」添加标签即可。

Figure 7: 给设备打标签

Figure 7: 给设备打标签

然后,可以添加规则了,切换到「Access controls」标签页修改规则,我的规则如下:

"hosts": {
    // 局域网服务器 Tailscale IP
    "deb":        "100.111.111.111",
    // 局域网服务器子网网段
    "deb-lan":    "192.168.1.0/24",
    // 局域网服务器本地 IP
    "deb-server": "192.168.1.33",
},

"acls": [
    // 普通用户的设备可以访问局域网服务器子网网段下的所有服务以及所有出口节点
    {
        "action": "accept",
        "src":    ["tag:user"],
        "dst":    ["deb-lan:*", "autogroup:internet:*"],
    },
    // 访客的设备只允许访问局域网服务器的 53、80、443 端口,53 是 DNS 服务端口
    {
        "action": "accept",
        "src":    ["tag:guest"],
        "dst":    ["deb-server:53", "deb-server:80", "deb-server:443"],
    },
    // 所有设备都可以访问中继服务器
    {
        "action": "accept",
        "src":    ["*"],
        "dst":    ["tag:derper:*"],
    },
],

我这里实测需要允许所有设备访问中继服务器,不然设备无法访问局域网域名(按说这个中继服务器只是做流量转发的,不应该影响组网,搞不清楚原因)。修改后点保存即可生效。在手机上使用数据网络重启 Tailscale 即可预览效果。

这里还是要多提一句,没有任何一种方式可以让你的服务器免受互联网上的各种类型的攻击。访问控制只能一定程度上保护你的内网服务器,要实现真正的数据安全,还是要加强防护意识,做好备份,避免使用弱口令无鉴权等不安全的方式。

尾巴

在折腾过程中,碰到不少问题,从很多大佬的博客中得到帮助,因此也把折腾经历写出来,帮助有同样需求的朋友。

参考资料