浅探 Tailscale DERP 中转服务

一、简介

tailscale 是一个很好用的工具,它包含了多种高级特性(例如 Magic DNS)来方便用户的使用,主要用于异地组网。

这也是本人抛弃 Zerotier 选择 Tailscale 的缘故,高级特性很多用的很方便。

tailscale 的底层机制与 zerotier 不同。

  • zerotier 会让每个客户端在启动时立即尝试与其他客户端的打洞,并一直维持这个连接。
    • 优点:创建链接时可以非常快速。要么早已打洞完成,要么就是百分百确定走中继节点。
    • 缺点:需要维护与所有对等节点的打洞链接,占用资源。节点一多则维护打洞的开销就比较大。
  • tailscale 只会在需要与 peer 建立连接的时候才会尝试打洞,而且最开始的流量一定是会经过 DERP 中转服务器。(非常的 Lazy…)
    • 优点:懒加载机制无需预先维护与其他节点的任何打洞连接,无需预先维护任何状态。
    • 缺点:每次通过 tailscale 创建虚拟连接时,初始所创建的连接其延迟很高,这会极大的影响使用体验;tailscale 极其依赖中继节点

而在 P2P VPN 中,自建中继节点是相当重要的。一方面自建中继节点可以比地理位置较远的官方中继节点更好的观察和协调本地两台对等机的 p2p 过程,另一方面可以在打洞失败后快速中继和转发流量

本人先前的文章已经介绍了 Zerotier 搭建中继节点 Moon 的原理和过程。Zerotier 会在特定 Primary Port 9993 上监听 UDP 连接来中继数据,因此在实际搭建的过程中只需将这一个 UDP 端口暴露至公网即可,要求极低。而暴露端口有多种方式可以实现,例如内网穿透 FRP 等等,也因此 moon 节点甚至都不需要有一个属于自己 IP 地址

注:Zerotier 不支持自建 TCP 中继,moon 节点实际上只是一个 UDP 中继节点。

而 Tailscale 的中继服务器(称为 DERP 服务)的搭建与 zerotier 相比存在一点困难,而网络上的搭建教程真是参差不齐(跟我之前找 Zerotier Moon 的搭建过程一样难顶,这点要狠狠吐槽一下)。那么接下来,我们来尝试找到一种最简便的方式来构建 tailscale DERP 服务器,顺带学习一下 DERP 服务的一些原理。

Tailscale 的中继 DERP 服务就是一个 TCP 中继节点,与 Zerotier 完全相反。

TL,DR: 如想跳过前置内容,直接快速了解搭建过程,请直接跳转至本文最后一节的总结部分

看完本文,你将了解到无需公网机器、无需域名、无需证书、无需修改源代码、无需自托管 HeadScale 服务的情况下,只需1-2个端口,来快速构建 Tailscale-DERP 服务。

需要注意的是本篇文章只考虑 tailscale 而不考虑 headscale,因为我希望使用的过程中能够尽可能简便,不想单独部署一个 headscale 控制服务器。

二、初探 DERP 服务的初始要求

Tailscale 官方文档说明了 DERP 服务器需要满足一些要求:

  1. 需要能够公网访问。这是为了让各个 Tailscale 节点可以直接访问到该 DERP 服务器,以此来便于进行后续的流量转发等操作。这个要求非常正常。

  2. 需要运行 HTTPS 服务。本质上是为了在传输数据给 DERP 服务器时数据可以通过 TLS 加密。

    HTTPS 服务通常需要 带有一个 TLS 证书。DERP 服务器只认 Let‘ s Encrypt 这家服务商颁发的证书,但该服务商不会给纯 IP 的服务器颁发证书。这实际上就隐含了一个条件:还需要一个公共域名

    这里的 TLS 加密和 Tailscale P2P 加密不同,前者是加密 peer to server 的流量,后者是加密 peer to peer 的流量。换句话说,TLS 要加密的数据中会包含(已经被 tailscale peer 加密过的)待中继加密流量

  3. 必须分配 80 端口来运行 HTTP 服务。这个要求很强烈,限制死了端口。

  4. 需要额外暴露两个端口来运行 HTTPS 和 STUN 服务

  5. 必须允许 ICMP 流量的出入。Tailscale Document 中用的是 must 来指定其重要性,但个人感觉应该是不需要这个要求。

上面从文档中总结出来的几点要求可能不太准确,因为网络中有部分文章介绍了搭建纯 IP DERP 服务器的过程(但是还是什么介绍都没有,看了跟没看没什么两样……)。不过从我阅读代码得到的经验看来官方文档上这方面内容很有可能已经过时,实际应该不需要这么强的要求

需要注意的是 DERP 服务要求服务器上需要携带 TLS 证书主要还是出于数据加密的目的;但在 Tailscale 节点中,两个节点在传输数据前会使用各个节点事先已经上传至 Tailscale 中心节点(即协调服务器)里的公钥来做加密。因此,DERP服务器要求的 TLS 证书的实际作用是为了隐藏数据转发的这个行为本身。由于本人自建节点主要是自己使用,因此这个隐藏就显得比较无所谓。

那么这样一来就有一个有意思的问题:

能否在最少操作、最少要求的情况下来做 DERP 中继?

那这就要深入到 DERP 的实现原理了。

三、初探 DERP 原理

当前使用的 tailscale 版本为 v1.52.1 (2023/11/11),git commit 为 86c8ab75.

1. DERP 配置相关

DERP 的顶层实现主要由两个文件组成

从 DERP 服务器的代码中可以收获一些有意思的东西:

  1. DERP 服务器可以同时运行两个服务,一个是使用 HTTP/HTTPS(TCP 协议)的 DERP 数据中转服务;另一个是使用 UDP 协议的 STUN 打洞服务。

    这俩服务刚好使用了不同的运输层协议,所以应该可以把 Zerotier 那套机制拿过来用。

  2. 所绑定的 IP、HTTP/HTTPS(DERP 服务) 监听端口、选择指定 HTTP 还是 HTTPS 协议、以及 STUN 监听端口都是可配置的,灵活性很好。

  3. 可以指定参数 verify-clients 来限制使用当前 DERP 服务的只能是自己的 tailscale 节点,防止白嫖。不过启用该服务需要当前 DERP 服务器本身就是一个 tailscale 节点,或者存在 socket 文件 /var/run/tailscale/tailscaled.sock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// cmd/derper/derper.go

var (
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443")
runSTUN = flag.Bool("stun", true, "whether to run a STUN server. It will bind to the same IP (if any) as the --addr flag value.")
runDERP = flag.Bool("derp", true, "whether to run a DERP server. The only reason to set this false is if you're decommissioning a server but want to keep its bootstrap DNS functionality still running.")

meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list")
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")

acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
)

这些信息说明 DERP 的可配置性很高,且对 TLS 的要求是可选的。不过只知道 DERP 服务可以选择开启 HTTP 协议还不够用,我们还需要看看各个客户端节点是如何配置与使用 DERP 服务的,因为假如客户端节点强制启用 TLS 访问 DERP 服务,那即便关掉 DERP 服务的 TLS 也无济于事

http-port 参数只会在启用了 HTTPS 服务后,才会尝试监听新的 HTTP 服务(cmd/derper/derper.go#L284)。这意味着 HTTP 服务实际上不是必须的,官方文档里所提出的要求存在冗余。

2. DERP Client 配置相关

代码 tailcfg/derpmap.go 展现了下发至 client 上关于 derp 服务的配置信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// tailcfg/derpmap.go

// DERPNode describes a DERP packet relay node running within a DERPRegion.
type DERPNode struct {
// Name is a unique node name (across all regions).
// It is not a host name.
// It's typically of the form "1b", "2a", "3b", etc. (region
// ID + suffix within that region)
Name string

// RegionID is the RegionID of the DERPRegion that this node
// is running in.
RegionID int

// HostName is the DERP node's hostname.
//
// It is required but need not be unique; multiple nodes may
// have the same HostName but vary in configuration otherwise.
HostName string

// CertName optionally specifies the expected TLS cert common
// name. If empty, HostName is used. If CertName is non-empty,
// HostName is only used for the TCP dial (if IPv4/IPv6 are
// not present) + TLS ClientHello.
CertName string `json:",omitempty"`

// IPv4 optionally forces an IPv4 address to use, instead of using DNS.
// If empty, A record(s) from DNS lookups of HostName are used.
// If the string is not an IPv4 address, IPv4 is not used; the
// conventional string to disable IPv4 (and not use DNS) is
// "none".
IPv4 string `json:",omitempty"`

// IPv6 optionally forces an IPv6 address to use, instead of using DNS.
// If empty, AAAA record(s) from DNS lookups of HostName are used.
// If the string is not an IPv6 address, IPv6 is not used; the
// conventional string to disable IPv6 (and not use DNS) is
// "none".
IPv6 string `json:",omitempty"`

// Port optionally specifies a STUN port to use.
// Zero means 3478.
// To disable STUN on this node, use -1.
STUNPort int `json:",omitempty"`

// STUNOnly marks a node as only a STUN server and not a DERP
// server.
STUNOnly bool `json:",omitempty"`

// DERPPort optionally provides an alternate TLS port number
// for the DERP HTTPS server.
//
// If zero, 443 is used.
DERPPort int `json:",omitempty"`

// InsecureForTests is used by unit tests to disable TLS verification.
// It should not be set by users.
InsecureForTests bool `json:",omitempty"`

// STUNTestIP is used in tests to override the STUN server's IP.
// If empty, it's assumed to be the same as the DERP server.
STUNTestIP string `json:",omitempty"`

// CanPort80 specifies whether this DERP node is accessible over HTTP
// on port 80 specifically. This is used for captive portal checks.
CanPort80 bool `json:",omitempty"`
}

我们既可以指定客户端在连接 DERP 服务器时所使用关于 DERP 服务和 STUN 服务的监听端口,也可以通过测试用的参数来让客户端在连接 DERP 服务器时指定是否启用 TLS 验证(即指定是否使用 TLS 证书)。管中窥豹,看上去好像客户端这块可配置性也比较强。

不过注意:从 DERP Client 的配置文件只能看出可以尝试用 TLS-Insecure 来连接 DERP 服务器的 HTTPS 服务并没有说明可以直接连接 DERP 服务器的 HTTP 服务。这个需要继续探究。

3. DERP 服务连接逻辑

那么 tailscale 节点是如何实际与 DERP 服务器进行交互的呢?需要关注这三个文件,层次依次从高到低:

  • wgengine/magicsock/magicsock.go:关键函数 sendAddr 支持向 DERP 服务器或直连 peer 发送单个数据包。该文件整体上提供了更新 endpoints、维护网络状态、发送与接收数据包的顶层实现、打洞路径探索等更为高层的功能特性。
  • wgengine/magicsock/derp.go:该文件中的关键主要函数为 derpWriteChanOfAddr,创建与单个 DERP 服务器的复杂连接,并进行信息(message) 处理。
  • derp/derphttp/derphttp_client.go:实现了底层与 DERP 服务器实际监听、连接、发送与接收数据(data) 等底层操作。

这些文件里面内容较多,就不细讲了,自己看看会更能感受到其中的内在精妙。

我们主要关注的是最后一个文件,因为底层操作才是影响我们能否用 non-TLS 协议创建连接的关键位置(即 HTTP 协议,毕竟要是能走 HTTP,那就没必要走关闭证书验证的伪 HTTPS 协议) 。

正常情况下,tailscale 都只会与同个 Region 中的其中一个节点进行连接和通信,即便单个 Region 里存在多个冗余节点,tailscale 也只会连接其中一个:

  1. 客户端尝试创建 DERP Region Client:derpWriteChanOfAddr 函数 - wgengine/magicsock/derp.go#L321

  2. 实际创建 DERP Region 的 Client 结构体:NewRegionClient 函数 - derp/derphttp/derphttp_client.go#L109

  3. 客户端向 DERP Region 发起连接,获取 TCP 连接(注意不是 TLS 连接):dialRegion 函数 - derp/derphttp/derphttp_client.go#L570

  4. 在获取 DERP Region 的 TCP 连接后,根据条件判断选择是否使用 HTTPS 协议:derp/derphttp/derphttp_client.go#L392 & derp/derphttp/derphttp_client.go#L426

    以下代码先走 switch-case 的 default 分支,之后进入 c.useHTTPS() 语句判断当前是否使用 HTTPS 协议进行连接。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    func (c *Client) connect(ctx context.Context, caller string) (client *derp.Client, connGen int, err error) {
    ...

    var node *tailcfg.DERPNode // nil when using c.url to dial
    switch {
    case useWebsockets():
    ...
    case c.url != nil:
    c.logf("%s: connecting to %v", caller, c.url)
    tcpConn, err = c.dialURL(ctx)
    default:
    c.logf("%s: connecting to derp-%d (%v)", caller, reg.RegionID, reg.RegionCode)
    tcpConn, node, err = c.dialRegion(ctx, reg)
    }
    if err != nil {
    return nil, 0, err
    }

    ...

    var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
    var serverPub key.NodePublic // or zero if unknown (if not using TLS or TLS middlebox eats it)
    var serverProtoVersion int
    var tlsState *tls.ConnectionState
    if c.useHTTPS() {
    tlsConn := c.tlsClient(tcpConn, node)
    httpConn = tlsConn

    // Force a handshake now (instead of waiting for it to
    // be done implicitly on read/write) so we can check
    // the ConnectionState.
    if err := tlsConn.Handshake(); err != nil {
    return nil, 0, err
    }
    ...
    }
    ...
    }

Client.useHTTPS 函数就是客户端用来判断连接 DERP 服务器时是否需要使用 HTTPS 协议,从下面的代码中可以得知,当客户端连接 DERP 服务器时,它几乎一定会使用 HTTPS 协议。很简单,因为 DERP Region Client 的 url 字段是空的,除非启动调试参数,否则它就会使用 HTTPS。

手动在运行 DERP 服务时启用调试参数/修改源代码是比较 dirty 的,个人不太倾向这种操作,尽量能不改代码就尽量不改代码。因此这里我选择启用 HTTPS 协议算了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// derp/derphttp/derphttp_client.go
// --------------------------------

// Client is a DERP-over-HTTP client.
//
// It automatically reconnects on error retry. That is, a failed Send or
// Recv will report the error and not retry, but subsequent calls to
// Send/Recv will completely re-establish the connection (unless Close
// has been called).
type Client struct {
...

// Either url or getRegion is non-nil:
url *url.URL
getRegion func() *tailcfg.DERPRegion

...
}
...

// debugDERPUseHTTP tells clients to connect to DERP via HTTP on port
// 3340 instead of HTTPS on 443.
var debugUseDERPHTTP = envknob.RegisterBool("TS_DEBUG_USE_DERP_HTTP")

func (c *Client) useHTTPS() bool {
if c.url != nil && c.url.Scheme == "http" {
return false
}
if debugUseDERPHTTP() {
return false
}

return true
}

那么现在下结论:在正常情况下,DERP 中继服务一定走(可以不经过 TLS 验证的)HTTPS 协议

顺带讲一下 derphttp.Client 中 url 字段能否为 http。唯一一个创建带有 url 字段 Client 结构的函数的调用点如下,从中可以看到,URL 的 scheme 已经被限制死为 https 了,这个功能应该是给 tailscale 官方域名使用,因此对我们来讲用处其实不大:

1
2
3
4
5
6
7
8
9
// cmd/derper/mesh.go
func startMeshWithHost(s *derp.Server, host string) error {
logf := logger.WithPrefix(log.Printf, fmt.Sprintf("mesh(%q): ", host))
c, err := derphttp.NewClient(s.PrivateKey(), "https://"+host+"/derp", logf)
if err != nil {
return err
}
...
}

4. WebSocket

我在阅读 DERP 代码时,发现它也支持中继 websocket 流量,这引起了我的好奇。

在最初的时候,我以为 DERP 是通过 HTTPS 的 websocket 来中继流量。但经过一番消息查阅和代码阅读,发现事情其实并非这样,而是 tailscale 也支持 p2p 的 websocket 通信

开发者尝试让 Tailscale 可以运行在浏览器中,这个就有点意思了。那么目前有哪些有意思的浏览器项目可以和 Tailscale 结合呢?我发现了这个 - How we added full networking to WebVM via Tailscale

一句话描述:WebVM 是一个运行在浏览器中的小且精的 Linux VM,tailscale 和 WebVM 一拍即合使得我们可以在浏览器中通过 WebVM 直接访问我们的 tailscale 网络

这个东西极大的引起了我的好奇心,因为我一直在想要不要单独给机器暴露一个端口用来搭建 Web ssh,以便于在陌生机器上仍然能够访问我的 tailscale 网络。

而现在,我便可以通过浏览器上的 WebVM,使用 ssh 连接进远程机器进行操作,完美满足我的要求,非常的 nice。

截一个使用示例,非常的有趣。WebVM 地址为 https://webvm.io/

Untitled

在 WebVM 会话存活时, tailscale 网络中会临时加入这台 VM,在该会话死亡时自动从 tailscale 网络中清除:

Untitled

但比较悲伤的是,WebVM 中的 tailscale 还不太支持 MagicDNS,也就是说得手输入 IP 地址连接远程机器了,没法用主机名。

5. STUN 服务

STUN 是用来进行 NAT 检测的服务。理论上说,将 STUN 放到与两台 peer 越近的位置越好,因为这能减少 NAT穿透的层数。但 STUN 原始协议要求 STUN server 至少拥有两个公网 IP 才能做到非常完备的 NAT 协议检测,因为两个公网 IP 可以让 STUN server 有两个流量出站口,便于模拟出“两台”设备来更好的检测 peer 的 NAT 类型。

但这种条件未免过于苛刻,一个公网 IP 都不太容易拿到,更何况是两个公网 IP,而且还得是两个公网 IP 都绑定在一个设备上,难上加难。不过好消息是 Tailscale 的 stun 并不需要这么高的要求,它的 STUN UDP 服务只做一件事:接受 peer 的 UDP 连接,并告诉 Peer 当前所看到 NAT 的 IP:Port 对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// cmd/derper/derper.go
func serverSTUNListener(ctx context.Context, pc *net.UDPConn) {
...
for {
// 1. 从 UDP 连接中读取 pkt
n, ua, err = pc.ReadFromUDP(buf[:])
if err != nil {
...
continue
}
// 2. 将 pkt 解析成 txid,主要是防止消息错位
pkt := buf[:n]
...
txid, err := stun.ParseBindingRequest(pkt)
...
// 3. 将 Server 从 UDP 连接中看到的公网 IP:Port,与 txid 打包发回给 client
addr, _ := netip.AddrFromSlice(ua.IP)
res := stun.Response(txid, netip.AddrPortFrom(addr, uint16(ua.Port)))
_, err = pc.WriteTo(res, ua)
...
}
}

这里的 STUN 服务非常简单,它只做了整个 NAT 检测中最简单的一环,那就是告诉 client 来自 server 的公网视角

但打洞没有这么简单,Tailscale 还会基于 DERP 服务的 discovery message 旁路信道 + 其他 NAT 检测的黑科技来做 NAT 穿越

这里就得插一个 Tailscale blog 链接了,最好是点进去看:How NAT traversal works NAT - Tailscale Blog,不过我更推荐看这个译文:[译] NAT 穿透是如何工作的:技术原理及企业级实践(Tailscale, 2020)- arthurchiao’s blog,讲的非常的通俗易懂。

这里不打算介绍 Tailscale 打洞的一整套逻辑,因为关注点还是在于建立 DERP 服务,具体细节可以看上面的文章,而且因为过于黑科技以至于想简短讲完不太可能。不过在这里提到了 STUN 服务只是想说明 Tailscale 的 STUN 服务只需要一个开放的 UDP 端口即可,再没有其他苛刻的条件了

四、DERP 测试搭建

1. 安装 derper 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 去 https://go.dev/dl/ 下载最新版(一定要下载版本,而非 apt-get install golang)
wget https://go.dev/dl/go1.21.3.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.3.linux-amd64.tar.gz
rm go1.21.3.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# 确定安装是否成功
go version
# 查看 GOROOT 和 GOPATH 是否不为空 & 可访问
go env

# 配置 go 代理并安装
go env -w GOPROXY=https://goproxy.cn,direct
go install tailscale.com/cmd/derper@latest
# 安装 derp probe 协助测试 derper
go install tailscale.com/cmd/derpprobe@latest

2. 创建自签名证书

创建自签名证书主要是糊弄 derper 用的,让它运行 HTTPS 服务;也可以改 derper 代码来绕过这个限制,但这么做后续也不方便更新 derper。

创建自签名证书有几个注意点:

  1. 先随便想一个 HostName,这里我想的是 kiprey-derp。但是要注意这个 HostName 一定要记住,后面证书签名包括请求访问等等都会用到。
  2. 证书生成后,私钥文件和证书文件名的前缀都要改为 HostName
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
mkdir ~/certdir && cd ~/certdir
# 1. 生成私钥
$ DERP_HOST="kiprey-derp"
$ openssl genpkey -algorithm RSA -out ${DERP_HOST}.key
...

# 2. 生成证书请求 (CSR):
$ openssl req -new -key ${DERP_HOST}.key -out ${DERP_HOST}.csr
# 一路放空按 enter 即可。

# 3. 生成自签名证书,设置过期期限为 100 年,防止后续再重新操作
$ openssl x509 -req \
-days 36500 \
-in ${DERP_HOST}.csr \
-signkey ${DERP_HOST}.key \
-out ${DERP_HOST}.crt \
-extfile <(printf "subjectAltName=DNS:${DERP_HOST}")

# 4. 查看生成的证书
$ openssl x509 -in ${DERP_HOST}.crt -noout -text
Certificate:
Data:
Version: 3 (0x2)
...
Issuer: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
Validity
Not Before: Nov 12 05:17:34 2023 GMT
Not After : Oct 19 05:17:34 2123 GMT
Subject: C = AU, ST = Some-State, O = Internet Widgits Pty Ltd
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:kiprey-derp
...
...

3. 运行 derper 服务

1
2
3
4
5
6
7
8
9
10
# 启动 derper
# 因为 derp 在启用 HTTPS 后会自动监听 HTTP,所以指定 HTTP PORT 为 -1 将其禁用
~/go/bin/derper \
-c ~/.derper.key \
-a :8888 -http-port -1 \
-stun-port 8889 \
-hostname ${DERP_HOST} \
--certmode manual \
-certdir ~/certdir \
--verify-clients

4. 测试连通性

以下是 HTTPS 协议 DERP 服务的连通性测试过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unset all_proxy http_proxy https_proxy
# --insecure 表示使用 TLS-Insecure
# --resolve 表示将 DERP_HOST 绑定至本地的 127.0.0.1
$ curl --insecure --resolve "${DERP_HOST}:8888:127.0.0.1" "https://${DERP_HOST}:8888"
<html><body>
<h1>DERP</h1>
<p>
This is a
<a href="https://tailscale.com/">Tailscale</a>
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
server.
</p>
<p>Debug info at <a href='/debug/'>/debug/</a>.</p>

# 测试 UD 协议 STUN 服务的连通性
$ nc 127.0.0.1 8889 -v -u
Connection to 127.0.0.1 8889 port [udp/*] succeeded!

测试的时候一定要关闭代理!不然访问 localhost 就会走代理,导致:
curl: (35) error:0A000126:SSL routines::unexpected eof while reading

Untitled

需要注意的是,在访问 DERP 的 HTTPS 服务时,只能用之前指定的 DERP_HOST 这个 HostName 来进行访问,因为 DERP 服务会对 Client 的连接进行校验,确保 Client 发送来的 ServerName 与本地证书的 HostName 一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// cmd/derper/derper.go
// --------------------
func main() {
...
if serveTLS {
log.Printf("derper: serving on %s with TLS", *addr)
var certManager certProvider
certManager, err = certProviderByCertMode(*certMode, *certDir, *hostname)
if err != nil {
log.Fatalf("derper: can not start cert provider: %v", err)
}
httpsrv.TLSConfig = certManager.TLSConfig()

// 1. 会在 Client 连接时尝试从 Client Hello 信息中获取证书
getCert := httpsrv.TLSConfig.GetCertificate
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
cert, err := getCert(hi)
if err != nil {
return nil, err
}
cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil
}
...
...
}

// cmd/derper/cert.go
// ------------------
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
// 2. 在获取证书时会先判断 Client 请求的 ServerName 是否与本地指定的 hostname 一致
if hi.ServerName != m.hostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}

// Return a shallow copy of the cert so the caller can append to its
// Certificate field.
certCopy := new(tls.Certificate)
*certCopy = *m.cert
certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
return certCopy, nil
}

因此在测试的时候,如果是使用 curl 访问则需要指定 --resolve 参数,来让发往 DERP_HOST 的请求最终能 resolve 到本地地址:

curl --insecure --resolve "${DERP_HOST}:8888:127.0.0.1"https://${DERP_HOST}:8888”

如果我们直接用浏览器打开的话,页面还是比较简洁的:

注:想用浏览器打开该 HTTPS 服务,要么做地址绑定,要么再建一个 DERP_HOST=localhost 的证书,此处不再赘述。

Untitled

点击 /debug,这将会打开一些调试用的数据页面,如果我们再进一步的点击,就可以发现在源代码里经常设置的调试字段。我们可以利用这里的字段来间接判断 DERP/STUN 是否工作正常。

Untitled

这个非常有用,因为我们可以通过这种方式来确认 DERP 服务和 STUN 服务是可以指定同一个端口并正常工作的(因为两个服务一个使用 TCP 一个使用 UDP):

nc 上去后初始时 not_stun 值为 5,在发送三行数据后值变为了 8。

Untitled

那么 Zerotier 那一套操作就可以直接套在 Tailscale DERP 服务器上了(mix-port)。

5. DEBUG 防护

出于安全性的考虑,我们希望在实际部署时关闭掉这个 debug 模式,那该如何操作?

这个实际上已经不需要我们操心,从 tsweb/tsweb.go#L53 中可以看出,它只会为满足几个条件的 debug 请求放行:

  1. 请求来源为本地回环 IP、tailscale IP 以及 TS_ALLOW_DEBUG_IP 指定的 IP。
  2. 请求不为 GET 方式且携带 debugkey ,同时 debugkey 的内容与 TS_DEBUG_KEY_PATH 所指定文件的内容相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// AllowDebugAccess reports whether r should be permitted to access
// various debug endpoints.
func AllowDebugAccess(r *http.Request) bool {
if allowDebugAccessWithKey(r) {
return true
}
if r.Header.Get("X-Forwarded-For") != "" {
// TODO if/when needed. For now, conservative:
return false
}
ipStr, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return false
}
ip, err := netip.ParseAddr(ipStr)
if err != nil {
return false
}
if tsaddr.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == envknob.String("TS_ALLOW_DEBUG_IP") {
return true
}
return false
}

func allowDebugAccessWithKey(r *http.Request) bool {
if r.Method != "GET" {
return false
}
urlKey := r.FormValue("debugkey")
keyPath := envknob.String("TS_DEBUG_KEY_PATH")
if urlKey != "" && keyPath != "" {
slurp, err := os.ReadFile(keyPath)
if err == nil && string(bytes.TrimSpace(slurp)) == urlKey {
return true
}
}
return false
}

实际测试如下:

Untitled

6. 编写 DERP-MAP

在本文章中,为了区分开 DERP 和 STUN 服务的不同,这两个服务暂不指定至相同的端口

DERP map 的编写可以参考官方: derp-map - tailscale

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"Regions": {
"233": {
"RegionID": 233,
"RegionCode": "useless-region-code",
"Nodes": [
{
"Name": "test-derp",
"RegionID": 233,
"HostName": "kiprey-derp",
"IPv4": "127.0.0.1",
"IPv6": "::1",
"DERPPort": 8888,
"STUNPort": 8889,
"InsecureForTests": true
}
]
}
}
}

注意:1. HostName 填写为先前确定的那一个 DERP_HOST,用于传递给 Server 校验;InsecureForTests 用于让客户端跳过证书校验。

将其保存为 derp-map.json 并运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ ~/go/bin/derpprobe -derp-map file://$HOME/derp-map.json -once
2023/11/12 15:01:38 Waiting for all probes (may take up to 1m)
2023/11/12 15:01:40 adding DERP TLS probe for test-derp ()
2023/11/12 15:01:40 adding DERP UDP probe for test-derp (derp/useless-region-code/test-derp/udp6)
2023/11/12 15:01:40 adding DERP UDP probe for test-derp (derp/useless-region-code/test-derp/udp)
2023/11/12 15:01:40 adding DERP mesh probe for test-derp->test-derp ()
2023/11/12 15:01:41 probe derp/useless-region-code/test-derp/tls: connecting to "kiprey-derp:443": dial tcp: lookup kiprey-derp on 127.0.0.53:53: server misbehaving
2023/11/12 15:01:47 probe derp/useless-region-code/test-derp/test-derp/mesh: derp.Recv: EOF
2023/11/12 15:01:54 good: derp/useless-region-code/test-derp/udp6: 667.205µs
2023/11/12 15:01:54 good: derp/useless-region-code/test-derp/udp: 2.71478ms
2023/11/12 15:01:54 good: derpmap-probe: 5.609009ms
2023/11/12 15:01:54 bad: derp/useless-region-code/test-derp/test-derp/mesh: derp.Recv: EOF
2023/11/12 15:01:54 bad: derp/useless-region-code/test-derp/tls: connecting to "kiprey-derp:443": dial tcp: lookup kiprey-derp on 127.0.0.53:53: server misbehaving

derpprobe 探测内容

先简单说明一下 derpprobe 探测的内容,它主要是探测以下三种功能(功能位于prober/derp.go):

  1. DERP TLS probe:只探测当前被测 DERP 服务器的 TLS 协议是否能正常建立 TLS 连接,不探测应用层数据(prober/derp.go#L58 & prober/derp.go#L97 & prober/tls.go#L29)。
  2. DERP UDP probe:探测当前被测 DERP 服务器上基于 UDP 的 STUN 服务是否正常(IPv4 & IPv6 各探测一次,会建立连接并收发数据)(prober/derp.go#L113 & prober/derp.go#L206)。
  3. DERP mesh probe:探测当前被测 DERP 服务器与同 Region 下其他 DERP 服务器的数据转发是否正常(prober/derp.go#L122 & prober/derp.go#L139 & prober/derp.go#L295)。

可以看到输出的结果里存在错误,其错误有两点:

  1. TLS 连接失败。阅读源代码发现 DERP TLS probe 连接 DERP 服务器的方式不太正宗,连接逻辑和常规客户端连接 DERP 服务器完全不同,并且只会请求访问配置中的 HostName 字段,而不会使用 IPv4/IPv6 字段(prober/derp.go#L97),同时还不使用 InsecureForTests 字段来设置关闭证书验证,因此 TLS probe 的错误就无法处理了;不过这个错误也无关紧要。

  2. Prober 连接 DERP 服务失败。DERP 服务一直报如下信息的错误:

    1
    2023/XX/XX XX:XX:XX derp: 127.0.0.1:50566: client xxxxx rejected: client nodekey:xxxxx not in set of peers

    通过调试发现是因为 prober 所使用的 client key 是随机生成的,因此 DERP 在指定 —verify-clients 后会将该 prober 连接阻断,在测试时需要去除 DERP 服务的这个参数,最终效果如下:

    Untitled

简单解释一下 Prober 的使用关键点:

  1. 上图中本人在运行 derpprobe 时是直接运行最新源代码,而非通过 go install 预编译二进制文件的形式。这是因为在本人使用 derpprobe 时,刚好 derpprobe 正在修复bug,最新版本的修复代码尚未提交至 go pkg,因此是直接运行的源代码。

  2. 使用 Prober 时一定要清除 proxy,否则你就会发现本该连接成功的 HTTPS 请求在一个奇怪的地方被”劫持“,导致 prober 失败:

    Untitled

那么,derpprobe 的测试到此为止,接下来要实际部署进 tailscale 网络中来进行测试。

DERP mesh probe 探测原理

顺带说一下 DERP mesh probe 的探测原理,这个比较有意思,其目的是测试不同 client 连接同一个 Region(Cluster)时的数据转发效果,这里尤其需要考虑不同 client 连接至同一个 Region 但不同 Region Node 时消息的转发状态。其测试过程如下:

为了避免混淆,规定 client1、client2 为非 DERP 服务的两个不同客户端节点;sclient1、sclient2 为 DERP 服务内对应创建的两个结构体,用来和 client1、client2 交互等等。

  1. 初始时,prober 会在 derpProbeNodePair 函数里创建出两个分别连接不同 Region 的 client 结构 client1(连接 derp1) 和 client2(derp2)。这两个 client 使用了不同的密钥对,以假装是两个独立 Node 来对不同 DERP 发起连接。

    但要知道的是,代码里只是传入了两个处于同一 Region 的不同 RegionNode,那该如何达到连接不同 Region 的目的呢?事实上,prober 会将这些 DERP 节点伪造成来自不同 Region 的节点(prober/derp.go#L364,注意所返回的 DERPRegion 的 Nodes 都是传入的单个节点,RegionID 相同没有影响)。

  2. 另一边,远程 DERP 服务会在收到 client 的连接请求后,调用 registerClient 函数

    1. 在 DERP 服务本地维护一个结构体 sclient,保存每个 client 连接的状态以及尚未发出的信息。

      DERP 这里一个 sclient 结构配对 Client 端的一个 derphttp.Client

    2. 向正在 watch 本 DERP 服务连接状态的其他 DERP Client 广播该 client 的上线情况(例如是否上线、远程 IP 地址信息等等,broadcastPeerStateChangeLocked 函数

  3. 接下来,prober 会令 client1 发送随机 8 字节数据给 client2,并期望能从 client2 中接收到相同的数据。数据的实际流向应该是 client1 → derp1 → derp2 → client2。具体来说:

    1. prober 在令 client1 发送数据时,client1 会调用 derp_client.Send 函数,在这个 data 前包裹上 frameSendPacket 枚举和 client2 的 dstKey 目的地址,使得构成一个 Frame packet
    2. 这个 Frame packet 将会被 client1 先发送给 derp1(因为 client1 不了解 client2 的地址)
    3. derp1 在接收到 Frame Packet 后,会进入 handleFrameSendPacket 函数进行处理。
      1. 假如 client1 和 client2 连接的是同一个 Region(即 derp1 和 derp2 是同一个,只是逻辑上我们将它们分开来),那么 derp1 事实上是拥有 client2 在 derp1 这里所对应的 sclient2 结构体,则 derp1 会直接发送 raw packet 给 sclient2。

        该 raw packet 里会附带上传入数据的原始来源节点的 key(实际上就是每个节点所持有的公钥),相当于是把数据来源方的 ID 保存在了 raw packet 里。

        sclient2 在接收到这个 raw packet 后会生成 frameRecvPacket 给 client2。如此 client2 便可以调用 Recv 函数来获取其他节点发送给 client2 的数据。

      2. 假如 client1 和 client2 连接的是独立的 Region,那么由于 derp1 也不知道 client2 的具体地址,它就会去获取知道 client2 地址的 fowarder 句柄(在这里是一个 连接着 derp2 的 sclient 结构体)。通过该 fowwarder 句柄将消息从 derp1 传输给 derp2,并由 derp2 来将消息传递给 sclient2,并最终发送给 client2。

        forward 操作只会执行一次,不会执行第二次。

        那么 derp1 是怎么知道要找 client2 得先找 derp2 呢?这就跟上面 2.b 提到的 client 状态广播机制有关。在启动 derp 服务时,参数中可以指定其他多个处于同一个 region Node 的 mesh 节点,derp 服务会依次向这些 mesh 内的 derp 节点发起连接,并 watch 这些节点的 client 连接状态,以维护 derp 服务的 fwd 状态。

        不过这些就太细节了,没什么必要追究的了。

五、Tailscale 调试环境搭建

如果需要单步调试相关逻辑的话,需要手动 git clone tailscale 仓库至本地来调试,不能直接用 ~/go/pkg/mod/tailscale.com@v1.50.1 底下的,因为这里的文件夹没有写权限

本人使用的 VSCode launch.json 如下,注意 program 一栏只能指定到文件夹,不能指定到具体的 go 代码,因为这会让调试器无法找到多文件项目中其他 go 代码,导致符号缺失:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch derpprobe",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "cmd/derpprobe",
"args": [
"-derp-map", "file:///home/kiprey/derp-map.json",
"-once"
],
"cwd": "${workspaceFolder}",
"env": {
// 去除代理设置
"ALL_PROXY": null, "all_proxy": null,
"HTTP_PROXY": null, "http_proxy": null,
"HTTPS_PROXY": null, "https_proxy": null,
}
},
{
"name": "Launch derp",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "cmd/derper",
"args": [
"-c", "/home/kiprey/.derper.key",
"-a", ":8888", "-stun-port", "8889",
"-http-port", "-1",

"-hostname", "kiprey-derp",
"--certmode", "manual",
"-certdir", "./certdir",
],
"cwd": "${workspaceFolder}",
}
]
}

六、DERP 搭建总结演示

这一节我将整合上面的所有内容,从头到尾以最短篇幅描述搭建一个 DERP 服务器的操作流程。

重申:当前使用的 tailscale 版本为 v1.52.1 (2023/11/11),git commit 为 86c8ab75.

这里重点说明一下 tailscale 版本,因为 Tailscale 迭代升级速度很快,可能一两年后该文章就不再适用了(捂脸)

1. 前置条件

只有一个要求,那就是一个允许通过 TLS 流量的 TCP 协议的公共信道以及一个 UDP 协议的公共信道

无需域名、无需 TLS 证书、无需修改任何源代码、也无需自行部署 Headscale 等等,找个内网穿透服务就能建。

这里说的比较抽象,实际上就是要么是一个 TCP 端口和一个 UDP 端口,要么就是一个端口同时允许 TCP 和 UDP 通信(mix-port)。如果不想运行 stun 服务只想搭建 derp 中转服务的话,则无需 UDP 端口。

但无论如何,TCP 端口都 不得限制 TLS 流量的通过,通常这种限制会来自于运营商(例如家用公网 IP 部署)或者内网穿透服务商(服务商要对穿透内容负责,因此可能需要实名认证等方式才能放行用户的 TLS 流量)。

2. 安装 DERP

以下所有命令全部在 DERP 服务器上运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 去 https://go.dev/dl/ 下载最新版(一定要下载版本,而非 apt-get install golang)
wget https://go.dev/dl/go1.21.4.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.21.4.linux-amd64.tar.gz
rm go1.21.4.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# 确定安装是否g成功
go version
# 查看 GOROOT 和 GOPATH 是否不为空 & 可访问
go env

# 配置 go 代理并安装
go env -w GOPROXY=https://goproxy.cn,direct
go install tailscale.com/cmd/derper@latest
# 安装 derp probe 协助测试 derper
go install tailscale.com/cmd/derpprobe@latest

3. 启动 DERP

配置端口暴露至公网。这一步既可以通过内网穿透完成,也可以配置已有暴露至公网的机器的 iptables 策略:

请注意:iptables 策略有优先级之分,一定要插到 DROP all 之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 配置 TCP 入站,将允许 dest-port 为 8888 的 TCP 连接规则插入 iptables 中的第 10 条
sudo iptables -I INPUT 10 -p tcp --dport 8888 -j ACCEPT
# 配置 UDP 入站,将允许 dest-port 为 8889 的 UDP 连接规则插入 iptables 中的第 10 条
sudo iptables -I INPUT 10 -p udp --dport 8889 -j ACCEPT

# --------------
# 查看 iptables
$ sudo iptables -L -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT icmp -- 0.0.0.0/0 0.0.0.0/0
...
ACCEPT udp -- 0.0.0.0/0 0.0.0.0/0 udp dpt:8889
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8888
DROP all -- 0.0.0.0/0 0.0.0.0/0

接下来,配置并启动 DERP 服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 指定 DERP_HOST 为 kiprey-derp(后面会用)
DERP_HOST="kiprey-derp"
DERP_PORT=8888
STUN_PORT=8889

# 创建自签名证书
mkdir ~/certdir && cd ~/certdir
openssl genpkey -algorithm RSA -out ${DERP_HOST}.key
openssl req -new -key ${DERP_HOST}.key -out ${DERP_HOST}.csr
openssl x509 -req \
-days 36500 \
-in ${DERP_HOST}.csr \
-signkey ${DERP_HOST}.key \
-out ${DERP_HOST}.crt \
-extfile <(printf "subjectAltName=DNS:${DERP_HOST}")

# 启动 DERP 服务(中继和stun)
# --verify-clients 需要本地运行 tailscaled,我在这里省略了安装 tailscale 的步骤
~/go/bin/derper \
-c ~/.derper.key \
-a :${DERP_PORT} -http-port -1 \
-stun-port ${STUN_PORT} \
-hostname ${DERP_HOST} \
--certmode manual \
-certdir ~/certdir \
--verify-clients

启动 DERP 服务后,在另一台机器上做连通性测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 这里的 DERP_HOST 要与 DERP 服务上的一致
DERP_HOST="kiprey-derp"

# 以下是 DERP 服务的公网视角,即如何从公网连接其地址和端口。
# 如果存在端口转发,则这里的端口会和上面 DERP 服务本地监听的端口不同,请自行配置
DERP_PUB_IP="a.b.c.d"
DERP_PUB_PORT=8888
STUN_PUB_PORT=8889

$ unset all_proxy http_proxy https_proxy
$ curl --insecure --resolve "${DERP_HOST}:${DERP_PUB_PORT}:${DERP_PUB_IP}" "https://${DERP_HOST}:${DERP_PUB_PORT}"

<html><body>
<h1>DERP</h1>
<p>
This is a
<a href="https://tailscale.com/">Tailscale</a>
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
server.
</p>

# 测试 UDP 协议 STUN 服务的连通性
$ nc ${DERP_PUB_IP} ${STUN_PUB_PORT} -v -u

Connection to a.b.c.d e port [udp/*] succeeded!

连通性测试通过后,DERP 服务器上先关闭 derp 服务,创建 service 来让它开机自启:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
DERP_HOST="kiprey-derp"
DERP_PORT=8888
STUN_PORT=8889

# 创建service文件
echo "[Unit]
Description=Tailscale derp service
After=network.target

[Service]
ExecStart=/home/${USER}/go/bin/derper \
-c /home/${USER}/.derper.key \
-a :${DERP_PORT} -http-port -1 \
-stun-port ${STUN_PORT} \
-hostname ${DERP_HOST} \
--certmode manual \
-certdir /home/${USER}/certdir \
--verify-clients
Restart=always
User=${USER}

[Install]
WantedBy=multi-user.target" \
| sudo tee /etc/systemd/system/tailscale-derp.service

# 重新加载Systemd配置
sudo systemctl daemon-reload

# 启动服务并设置开机自启动
sudo systemctl start tailscale-derp
sudo systemctl enable tailscale-derp

# 查看服务状态,没问题就行
# 如果有问题那就得看看是不是之前的 derper 忘记关了,导致端口占用
sudo systemctl status tailscale-derp

# -------------------
# 如需禁用
sudo systemctl stop tailscale-derp
sudo systemctl disable tailscale-derp

到这里后,DERP 服务配置完成。

4. 配置 ACL

接下来要去 Tailscale admin panel 网页,配置一下 ACL 以更新所有 tailscale 节点的配置信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
{
...
"acls": [...],
...
"ssh": [...],
...
"derpMap": {
"Regions": {
"900": {
"RegionID": 900,
"RegionCode": "MyDerp",
"Nodes": [
{
"Name": "MyDerp-Name",
"RegionID": 900,
"HostName": "kiprey-derp",
"IPv4": "a.b.c.d",
"DERPPort": 8888,
"STUNPort": 8889,
"InsecureForTests": true,
},
],
},
},
},
...
}

5. 演示

在网页上保存好 ACL 后,ACL 会立即下发到各个 tailscale 节点里。随便找个节点运行 netcheck,可以发现 DERP 成功添加:

Untitled

七、参考链接

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2024 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~