
"Tailscale without Tailscale, by Tailscale"
Tailcat
Tailcat是Tailscale开源组件的一个重制版,可以像netcat一样工作,使用Tailscale的数据面,但不使用Tailscale的控制面。Tailscale数据面(内部称为magicsock)为两台机器之间提供点对点的WireGuard®加密隧道,DERP则作为NAT穿透的通信备用通道,以及在NAT穿透失败时的最终中继。你可以不通过Tailscale控制面,随意以任何方式交换tailcat连接的元数据。
tailcatCLI基于tailcat Go库(可通过github.com/tailscale/tailcat导入)构建。
无论你是把tailcat当作CLI工具还是库来用,一端都会运行tailcat服务器(监听器)并获取一个临时令牌。另一端则把这个令牌传给tailcat的客户端来建立连接。双方之间的所有流量都会通过WireGuard进行端到端加密。初始连接会通过DERP服务器启动,然后magicsock会执行NAT穿透,在可能的情况下升级为直接的点对点UDP连接(正常情况)。
不需要Tailscale账号,也不需要管理员(root)权限,它不会更改你的路由表、DNS等配置, tailcat只是一个用户空间的库和命令行工具。
而且这一切都是开源的。
你可以使用我们免费的限速DERP中继(https://tailcat.dev/derpmap.json),或者你也可以自己搭建一个DERP。
这里有一个实验性的浏览器网页版演示(tailcat被编译成了WebAssembly),地址为https://tailscale.github.io/tailcat/,它可以发送和接收文件或文本,并与CLI互操作。浏览器的流量仅通过DERP中继,除非支持WebRTC才会直接连接。
安装tailcat
$ go install github.com/tailscale/tailcat/cmd/tailcat@latest
也可以通过Nix flakes方式运行或安装:
$ nix run github:tailscale/tailcat
$ nix profile install github:tailscale/tailcat
使用tailcat
标准输入/输出管道
启动监听服务,显示临时地址:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
(hangs, waiting...)
客户端发送内容:
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
服务端解析并显示内容:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
hello
通过隧道暴露本地端口
将本地TCP端口转发到localhost来提供服务:
$ tailcat --serve=8080,8443 # or --serve=all
# 🐈 Server listening with new address: tcXXXXXXXXX
客户端连接:
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: foo
HTTP/1.1 200 OK
....
免认证SSH服务
在Linux或macOS系统中,可以运行一个免认证的SSH服务。如果想要认证,运行tailcat --serve=22然后代理到你的系统SSH服务器即可。
$ tailcat --serve=no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
客户端连接:
$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
tailcat其他用法
用Ping来测试连接;每个pong会报告它是通过DERP中继连接还是直连。--until-direct 会不断ping(最多到--timeout,默认10秒),直到两者直连,如果没有则以非零状态退出:
$ tailcat ping --until-direct <token>
pong in 42.1ms via DERP(sfo)
pong in 1.2ms via 203.0.113.7:41641
通过隧道走SOCKS5代理运行命令:
$ tailcat socks <token> curl http://server.tailcat:8081/
令牌也可以作为URL主机名使用,SOCKS代理会识别并连接它们,所以token参数是可选的。令牌区分大小写,这在curl和大多数命令行工具中可用,但浏览器不行,因为浏览器会把主机名转换成小写。
$ tailcat socks curl http://<token>:8081/
充当出口节点(exit-node),让客户端可以访问服务器的网络:
$ tailcat --serve=exit-node
解析连接令牌并以JSON输出内容,包含服务器的WireGuard公钥和DERP信息:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"RegionID": 302
}
将短令牌(通过ID引用DERP域,要求客户端获取DERP地图)解析为一个更长的自包含令牌,其中嵌入了DERP服务器信息,让客户端能够更快连接:
$ tailcat resolve tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFpGQEu
tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
解析上面自包含令牌显示嵌入的DERP信息:
$ tailcat parse tcomFwWCCcjS5nKNqAod034nWoJZW0LZqDhhC8U_dKdnDRYQ8uNGFygaFhToGjYWhudGMzMDJhLmlwbi5kZXZhNG0yMDguMTExLjM5LjM4YTZzMjYwNzpmNzQwOjA6M2Y6OjcyMA
{
"ServerPublic": "nodekey:9c8d2e6728da80a1dd37e275a82595b42d9a838610bc53f74a7670d1610f2e34",
"Region": [
{
"Nodes": [
{
"HostName": "tc302a.ipn.dev",
"IPv4": "208.111.39.38",
"IPv6": "2607:f740:0:3f::720"
}
]
}
]
}
服务器可以直接使用 --full-address 参数打印完整的自包含格式。
密钥管理
服务器地址(连接令牌)是由它的WireGuard密钥生成的,所以你使用的密钥决定了谁能连接到你:
临时密钥(默认):每次服务器运行都会在内存中生成一个新的密钥,并打印出一个别人从未见过的地址。当进程退出时,密钥会被丢弃,地址也永远失效。
保存的密钥:
tailcat genkey会生成一个保存到磁盘的密钥,这样地址在重启后保持不变。可以通过--allow限制客户端(参见tailcat genkey --client)。
CLI启动时会显示它使用的秘钥类型,这样你就能知道自己是启动了一个新的临时服务,还是在重新监听以前使用过的地址。
$ tailcat genkey --region=nyc
# prints the token; key saved to ~/.config/tailcat/keys/default.private.json
# later; the key named "default" is used automatically once it exists:
$ tailcat --serve=8080
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
# ... unless you force a one-off ephemeral key:
$ tailcat --serve=8080 --key=new
# 🐈 Server listening with new address: tcXXXXXXXXX
default是一个魔法密钥名,一旦它存在,tailcat 就会悄悄地使用它,而不是生成临时密钥,上面那行启动信息告诉你发生了什么。
--key=new获取一个新密钥--key=<name>使用其他已保存的密钥tailcat genkey --delete --key=default删除保存的默认密钥tailcat genkey --list列出保存的密钥
令牌也可以作为DNS TXT记录发布,并通过名称查找;DNS名称可以在CLI使用令牌的任何地方使用:
# If example.com has a TXT record "tailcat=tc..."
$ tailcat example.com 8080
$ tailcat ssh example.com
$ tailcat ping example.com
tailcat用例
通过DNS保护SSH服务
运行一个可通过域名访问的SSH服务,服务端无需开放任何入站端口,WireGuard会在SSH服务接收到数据包之前完成客户端身份验证。
在客户端机器上,生成一个密钥对。它会打印出公钥,而服务端只需要知道这个公钥:
client$ tailcat genkey --client
# wrote file to ~/.config/tailcat/keys/client-default.private.json
nodekey:cfb6bfa77a0654d7450947fd6acef17d2cd848da1d30b2540b13dac272ddfd16
在服务端,生成一个固定到最近DERP域的密钥对(原因见下文),然后仅对该客户端提供SSH服务:
server$ tailcat genkey --fixed-region
# wrote file to ~/.config/tailcat/keys/default.private.json
tcXXXXXXXXX
server$ tailcat --serve=22 --allow=nodekey:cfb6bf...ddfd16
# 🐈 Server listening with saved key "default": tcXXXXXXXXX
修改DNS,将令牌以TXT记录添加。
my-server.example.com. 300 IN TXT "tailcat=tcXXXXXXXXX"
客户端可通过以下方式连接SSH服务。
client$ tailcat ssh my-server.example.com
客户端检测到已保存的client-default密钥时会自动使用它,因此无需额外参数。其他任何人的握手请求都会被静默忽略,他们无法访问SSH服务器,甚至无法得知其存在。
为什么用--fixed-region:它在生成密钥时探测最近的DERP域,并将ID固化到令牌和保存的密钥文件中,这样服务端重启后会绑定到同一DERP域,无需重新探测。
普通的tailcat genkey默认使用--region=auto,即启动时自动选择,适合临时使用。但发布到DNS的令牌应指定固定DERP域,以便客户端和未来的服务端重启都在同一地点汇合。
--region=<name>可显式指定区域--region=list查看可选列表
TODO:如果DERP映射随时间变化,需提升客户端的健壮性:https://github.com/tailscale/tailcat/issues/7
自建DERP中继服务器
你可以不使用Tailscale的官方中继服务,可以自建DERP中继服务器,然后将其主机名添加到--region=,生成使用该中继的服务端密钥:
server$ tailcat genkey --region=derp.example.com
tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
server$ tailcat --serve=22
这个令牌会嵌入你的中继主机名:
$ tailcat parse tcomFwWCCAIsKOqPUux6ClG2RM4A_vOq4VBzGgHGGjq9OsJuFKSWFygaFhToGhYWhwZGVycC5leGFtcGxlLmNvbQ
{
"ServerPublic": "nodekey:8022c28ea8f52ec7a0a51b644ce00fef3aae150731a01c61a3abd3ac26e14a49",
"Region": [
{
"Nodes": [
{
"HostName": "derp.example.com"
}
]
}
]
}
客户端无需额外参数,也不会连接Tailscale的DERP服务器或中继,唯一的速率限制由你自己决定。另外,如果你运营了一整套中继集群,可以提供自己的DERP映射JSON,并让两端通过--derpmap-url指向它。
Go库使用
一个极简服务器,通过隧道响应所有TCP端口,并打印令牌。零值Server会为未设置的内容选择默认值:一个新的临时密钥、默认DERP最近域,以及 log.Printf 日志记录(设置 Logf 为 logger.Discard 可以安静运行):
package main
import (
"fmt"
"log"
"net"
"github.com/tailscale/tailcat"
)
func main() {
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
return func(c net.Conn) {
fmt.Fprintf(c, "hello from port %v\n", port)
c.Close()
}
},
}
if err := s.Start(); err != nil {
log.Fatal(err)
}
fmt.Println(s.ConnBlob())
select {}
}
一个最简客户端,它会拨号,同时将这个令牌作为参数。就像Server一样,Client的零值只要设置了它的Server令牌字段(tailcat.NewClient 就是这种用法的简写),隧道会在第一次拨号时加载建立:
package main
import (
"context"
"io"
"log"
"os"
"github.com/tailscale/tailcat"
)
func main() {
cl := tailcat.NewClient(tailcat.ConnBlob(os.Args[1]))
defer cl.Close()
c, err := cl.DialTCPPort(context.Background(), 80)
if err != nil {
log.Fatal(err)
}
io.Copy(os.Stdout, c)
}
$ ./client tcomFwWCAWf933BLELdzd3RkHiOufJ...
hello from port 80
tailcat是怎么运作的
连接令牌
Tailcat服务器通过连接令牌(内部称为ConnBlob)来区分。格式如tcXYZ...,tc前缀加base64编码的CBOR,其中包含:
- 服务器的WireGuard公钥(Curve25519,32 字节)
- DERP信息,一个小整数,引用默认的Tailscale运行的tailcat服务器,或者完整的DERP服务器元数据,用于使用自定义DERP服务器,或者避免客户端可能需要往返获取最新的DERP地图(服务器的
--full-address标志和tailcat resolve子命令会生成这种形式)
一个只包含整数区域ID的典型令牌大约是50字节。加入嵌入的DERP节点细节后会更长,但它是自包含的。
tailcat网络技术栈
Tailcat重用了Tailscale的客户端网络组件,但未使用控制平面。
- WireGuard – 一个用户空间的WireGuard实现,用于加密所有隧道流量。它不使用内核TUN/TAP设备(也不配置任何网络路由或DNS设置),所以不需要
root权限。 - magicsock – Tailscale的传输层可以通过直接UDP和DERP中继来复用流量。它处理基于STUN的端点发现以及NAT穿透的UDP打洞。
- Netstack (gVisor) – 一个用户态的TCP/IP协议栈,它在进程内部终止TCP连接。这就是为什么Tailcat可以在不做任何操作系统网络配置的情况下接受入站连接和发起出站连接的原因。
- DERP relay – Tailscale的加密中继协议,用作会合通道,也作为当直接连接不可用时的备用数据路径。
tailcat连接流程
- 服务启动会生成(或加载)一个WireGuard密钥对,连接到DERP中继,并将其连接令牌打印到标准错误。然后等待客户端连接。
- 客户端解析令牌以获取服务器的公钥和DERP域。它生成自己的临时密钥对,并连接到同一个DERP中继。
- 发现握手,客户端通过DERP中继向服务器发送“喵”ping 消息。该消息携带客户端的节点公钥。服务器收到后,将客户端添加到其WireGuard对等列表和网络地图中,重新配置WireGuard引擎,并回复一个“喵过了”的确认。
- WireGuard隧道,双方都配置为WireGuard对等节点后,标准的WireGuard握手就会进行(最初通过DERP路由)。完成后,隧道建立,加密流量即可传输。
- NAT穿透,与此同时,每一方都会通过DERP在disco call-me-maybe消息中把自己的UDP端点(通过STUN获取的公共IP:端口,以及本地接口地址)广播给对方,并在信息变化时重新广播。然后双方会运行 Tailscale的disco协议并尝试UDP打洞。如果成功,流量就可以从DERP中继升级为直接的点对点路径。如果打洞失败,DERP会继续作为备用连接,连接仍然可以使用,只是在使用我们公共托管的DERP中继时,吞吐量会受到限制。
- 数据传输,客户端通过隧道连接服务器上的TCP端口。双方的
gVisor TCP/IP协议栈负责连接建立。在服务器端,传入的连接会根据端口分发到相应的处理器:转发到本地主机、输出到标准输出、运行SSH会话等等。
寻址
每个节点目前都是从它的WireGuard公钥生成一个确定性的IPv6地址,但这是一个实现细节,对最终用户不可见,而且可能会改变。(例如,我们可能会完全从IP头中移除这些字节,从而回收那些多余的MTU)
稳定性
Tailcat是免费的,但它不保证API或CLI的稳定性:Go API、CLI 参数和输出,以及传输格式都可能会变化。公开的限速Tailcat DERP中继没有正常运行时间SLA或吞吐量目标,我们可能随时以任何理由撤销访问权限。所有功能都是尽力提供的,没有任何合同关系(例如专用 DERP 中继和/或支持)另作保证。
tailcat历史
Tailcat的诞生可以追溯到2023年9月,当时它叫做derpcat,是在一次长途飞行中写的,同时还在补看一些烂片:第一个草图是提交9e4d925cc,第一次正常运行是在提交911915fbb(“derpcat: it’s alive!”,提交信息中提到“UA 605 PDX-ORD 飞往爱尔兰。耶,不买机上 Wi-Fi。”)。当时它存在于tailscale.com仓库的一个分支中,随着Tailscale内部代码的变化,它好几次陷入了腐烂。之后,我们又让它重获新生,并重构成了tailscale.com仓库的一个常规Go模块客户端,而不是仓库的一个分支。
tailcat它在2026年8月的TailscaleUp大会上正式发布。