手里有个实验需要自建一个内网的 DERP 来测试一项目的网络栈性能。由于我没有使用 Tailscale 官方的服务器作为 Tailnet 后端,而是采用 Headscale,手里又没有可用的域名和证书,于是不得不关闭 TLS 校验。过程中发现文档驳杂,踩坑颇多,遂记录。


我首先参阅了这几篇文档和 Blog:

我的需求是,在我自定义的端口上运行 DERP 服务器(以免和我本机的 80443 端口发生冲突),并且需要关闭客户端的 TLS 校验,因为我没有域名,也开不出有效的 TLS 证书。

这两个需求让我放弃了 Headscale Embedded DERP 这个好用的特性,因为 Headscale 的这个内嵌 DERP 服务器没有关闭 TLS 校验的能力,同时我也没摸索出来怎么指定这个 DERP 服务器的端口。

我先是将 derp.urls 置为 [] 禁用了 Tailscale 官方的 DERP 服务器。1 该参数是一个订阅列表,用于从远程获取可用的 DERP 服务器列表。默认值是 https://controlplane.tailscale.com/derpmap/default

derp.paths 是用于控制 DERP 来源的另一个 JSON 对象,它存储文件路径的列表,直接指向一个 yaml 文件。这个 yaml 文件的格式需要参阅 tailcfg 包的 DERPNode 定义。我采用的 yaml 文件如下。这个文件我们称之为derpyml

regions:
  901:
    regionid: 901
    regioncode: int
    regionname: internal
    nodes:
      - name: 901a
        regionid: 901
        ipv4: 192.168.24.4
        hostname: 192.168.24.4:50443
        stunport: 53478
        stunonly: false
        derpport: 50443
        insecurefortests: true

阅读我前面提到的几篇 Blog。它们不约而同地提及修改 Tailscale ACL 的步骤。不过对于 Headscale 而言,在 derp.paths 指定的 yaml 文件中进行配置即足矣。Headscale 会将这份 yaml 定义的 DERP 配置作为 ACL 的一部分下发到 Tailscale 客户端。

192.168.24.4是我部署了 Derper 的内网 IP。该设备上我使用这样的一个 docker-compose.yml 文件创建对应的 Derper 容器。

services:
  derper:
    image: ghcr.io/kaaanata/derper:latest
    container_name: derper
    restart: unless-stopped
    environment:
      - DERP_DOMAIN=192.168.24.4
      - DERP_CERT_MODE=manual
    ports:
      - '58080:80'
      - '50443:443'
      - '53478:3478/udp'
    volumes:
      - './cert:/app/certs'

我采用 kaaanata/derper 的这个镜像是有原因的。这个仓库是对go install tailscale.com/cmd/derper的直接封装。顺便一提,我其实还建议刚开始学习 Dockerfile 的用户阅读这个仓库的 Dockerfile,以学习 Docker 如何传递参数给容器。

第一个坑是 derpyml 中的 hostname。使用 IP 的作为 DERP 的接入点的时候,该 hostname 应该设置成 IP:port 的形式。 DERP 一般采用它的 443 端口进行通信。由于我们在 docker-compose.yml 里面将 443 端口映射到 192.168.24.450443 上,所以我们这里填写了 192.168.24.4:50443

第二个坑是DERP_CERT_MODE=manualkaaanata/derper 为这个参数采用的缺省值是 letsencrypt,即向 Let's Encrypt 申请证书。然而我们哪儿有什么域名可供证书申请!因而我们在这里的做法是将DERP_CERT_MODE设置为manual。阅读 kaaanata/derperDockerfile,我们知道这个变量被用于控制 Derper 的 --certmode 启动参数。将这个启动参数置为 manual 允许我们提供自签名证书。避免 Derper 向 Let's Encrypt 申请证书失败而阻塞。

kaaanata/derper 默认让我们将证书存放在 /app/certs/ 这个目录下。我在上面的docker-compose.yml里设置了相应的文件映射。使用下面的 Shell 指令,我们可以生成一份自签名证书到当前目录的 ./cert 目录下。

mkdir -p ./cert
export DERP_IP="192.168.24.4" # 改成你自己设备的 IP
openssl req -x509 -newkey rsa:4096 -sha256 -days 3650 -nodes -keyout ./cert/${DERP_IP}.key -out ./cert/${DERP_IP}.crt -subj "/CN=${DERP_IP}" -addext "subjectAltName=IP:${DERP_IP}"

我们需要在 derpyml 中设置 insecurefortests: true 以允许受试的 Tailscale 客户端跳过 TLS 检验。Headscale 的这个 derpyml 文件中的所有属性名都需要是小写。我试过在这里填写 tailcfg 包的 DERPNode 定义 提供的 InsecureForTests,但是客户端中没有下发相应字段。希望 Headscale 未来可以修复这个问题。

在客户端上我们可以通过 tailscale debug derp-map 2 检查 Tailnet 后端下发的 DERP 配置。理想情况下这个配置项应该和我们上述填写的 derp.yml 内容一致。这项检查可以帮助我们检查 derp.yml 是否完整地被下发到客户端。

tailscale debug derp headscale 这一测试是不必要的。该测试不受 InsecureForTests 影响,始终会进行 TLS 校验。我们只需要确认 tailscale statustailscale ping 工作正常即可。

以上。

  1. DERP - Headscale 

  2. (https://headscale.net/stable/ref/derp/?h=#check-derp-server-connectivity)