Tailcat 是 Tailscale 在 2026 年 8 月 TailscaleUp 大会上开源的网络连接工具,定位是 netcat 的替代品。它在两台机器之间建立 WireGuard 加密的连接,不需要注册账号、登录、组网,也不需要任何一方具备公网 IP 或开放端口。服务端启动后生成一个 tc 开头的连接 token,把 token 交给对方就能连上。项目用 Go 编写,采用 BSD-3-Clause 许可证,开源 4 天 GitHub stars 超过 3800,Hacker News 讨论帖拿到 662 points。
解决什么问题
netcat(nc)从上世纪 90 年代沿用至今,是 Unix 工具箱里最常见的网络工具之一,自称 “TCP/IP 瑞士军刀”,把两台机器的 stdin/stdout 接通就能传文件、调试端口、搬一个远程 shell。它的短板在加密和穿透:nc 本身不加密,两端只要有一端在 NAT 后面就连不上,要安全就得自己再叠一层 SSH 隧道。
这类临时连接的需求其实到处都是。给外包工程师开一小时数据库访问,任务结束权限就该收回;让 CI 任务回连内网机器取一次构建产物;给 AI agent 一台测试机的 shell,让它跑完验证就断开。共同特征是连接生命周期短,两端都不想为此维护一套长期的网络关系。
现有方案各自要付出代价:
| 方案 | 缺什么 |
|---|---|
| 裸 WireGuard | 没有 NAT 打洞,两端都在 NAT 后就废了,还需要固定公网端点 |
| Tailscale / Nebula / ZeroTier | 需要注册账号、身份体系、控制平面,接入前先组网 |
| Magic Wormhole | 只做文件传输,提供不了通用连接 |
| SSH 反向隧道 | 需要一台有公网 IP 的中间机器,还要开放端口 |
Tailcat 的取舍是只保留数据平面。Tailscale 产品由控制平面和数据平面组成,控制平面负责协调服务器、身份、ACL 策略这些治理能力,数据平面负责 WireGuard 加密、disco 打洞协议、DERP 中继这些传输能力。Tailcat 把控制平面整个剥掉,只留下数据平面,接入动作从“注册账号、安装、登录、组网”压缩到“跑一条命令,分享一个 token”。管道模型沿用 nc,换掉的是底下的传输层。Tailscale 对这个定位的说法是 “Tailscale without Tailscale, by Tailscale”。
来历
原型诞生于 2023 年 9 月,作者 Brad Fitzpatrick(Tailscale 联合创始人)在一次长途航班上写出了最初版本,当时叫 derpcat,第一次跑通的 commit message 里留着一行当时的备注:“UA 605 PDX-ORD en route to Ireland. yay not buying the wifi.”(航班上没买机上的 Wi-Fi。)
此后几年它一直活在 tailscale.com 主仓库的 fork 里,随着 Tailscale 内部代码演进几次失修(bitrot),后来重构成 tailscale.com 的常规 Go module 客户端,2026 年 8 月在 TailscaleUp 大会上正式开源。贡献者超过 70 人,包括 danderson、apenwarr 在内的多位 Tailscale 核心成员。当前最新版本 v0.2.0,2026 年 8 月 30 日发布,距离开源只过了 4 天。
工作原理
连接 token 就是地址。 服务端启动后打印一个 tc 开头的 token,约 50 字节,内部叫 ConnBlob,内容是 base64 编码的 CBOR 数据(一种紧凑的二进制序列化格式),包含服务端的 WireGuard 公钥(Curve25519,32 字节)和 DERP 区域信息。token 通过任意渠道传给对方,聊天窗口、邮件、DNS TXT 记录都行。客户端拿到 token 就有了发起连接所需的全部信息,不需要事先知道对方的 IP 或域名。
token 有两种形态。默认形态内嵌一个区域编号,客户端首次连接时需要拉取一次最新的 DERP map(中继节点列表);tailcat resolve 子命令或服务端 --full-address 参数生成自包含形态,把中继节点的完整信息直接写进 token,长度更长,但客户端连 DERP map 服务器都不用碰,连接更快。tailcat parse <token> 可以离线解析任意 token,把公钥和区域信息打印成 JSON,不发起任何连接。
连接建立分四步:
- 双方各自连到同一个 DERP 中继(Tailscale 运营的中继服务器网络)
- 客户端通过中继发送发现消息 “Meow”,附带自己的公钥,服务端回应 “Meowed”,并把客户端公钥加入 WireGuard peer 列表
- WireGuard 握手建立加密隧道,初期流量走中继
- 双方并行执行 STUN 探测(一种探测各自公网 UDP 地址的协议)和 UDP 打洞,成功后升级为 P2P 直连,失败则继续走 DERP 中继
DERP 承担的角色是牵线和兜底。多数网络环境下打洞会成功,实际流量走直连,不经过 Tailscale 的服务器;打洞失败(比如两端都在对称 NAT 后)才由中继转发,官方托管的中继有限速。默认 DERP 集群是 Tailscale 为 Tailcat 单独跑的一套,地址在 tailcat.dev/derpmap.json,域名和证书里都不出现 tailscale.com,与 Tailscale 主产品的中继网络分开。
纯用户态是第三个关键设计,网络栈的四个组件全部跑在用户态。wireguard-go 负责加密所有隧道流量;magicsock 是 Tailscale 的传输层,把直连 UDP 和 DERP 中继的流量复用在同一条连接上,STUN 探测和 UDP 打洞都在这一层完成;gVisor netstack 是用户态 TCP/IP 栈,在进程内终结 TCP 连接,让 Tailcat 不配置任何操作系统网络就能收发连接;DERP 中继协议承担牵线通道和兜底数据路径。整个程序不创建内核 TUN 设备(虚拟网卡)、不改路由表、不改 DNS、不需要 root 权限。一个静态二进制拷到目标机器就能跑,放进 Docker 容器也不需要特权模式。Tailscale 产品本身要动系统网络配置,Tailcat 什么都不碰,这是两者部署摩擦差距最大的地方。
每个 peer 的内部地址是从 WireGuard 公钥推导出的确定性 IPv6 地址,README 特别注明这是实现细节,未暴露给用户,后续版本可能改掉。
服务端启动时先打印选中的中继区域(如 “Selected bootstrap relay region 302, San Francisco”),再打印一行带 🐈 图标的监听地址。这些信息走 stderr,token 和状态输出不会混进 stdout 的数据流,默认管道模式下通道里只有数据,写脚本时不用做输出分离。
功能模式
默认模式把两端的 stdin/stdout 接通,行为与 nc 一致。
基本使用方法
最基础的用法是管道互通。机器 A 裸跑 tailcat 当服务端,它挂起等待连接,并打印自己的地址:
$ tailcat
# Selected bootstrap relay region 302, San Francisco
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZq...
(hangs, waiting...)
机器 B 用这个地址当参数,把数据送进管道:
$ echo hello | tailcat tcomFwWCCcjS5nKNqAod034nWoJZW0LZq...
机器 A 的进程解除阻塞,从 stdout 收到数据:
$ tailcat
# 🐈 Server listening with new address: tcomFwWCCcjS5nKNqAod034nWoJZW0LZq...
hello
$
两端管道里流什么由你自己决定,tar cz projects/ | tailcat 打包目录发过去、对端 tailcat <token> | tar xz 解开,就是一次端到端加密的文件传输。
第二个用法是把本地 TCP 端口暴露到隧道对端:
# 机器 A,暴露本地 8080 和 8443 端口(--serve=all 暴露全部)
$ tailcat --serve=8080,8443
# 🐈 Server listening with new address: tcXXXXXXXXX
# 机器 B,地址后面跟端口号,得到一条直通该端口的原始 TCP 连接
$ tailcat tcXXXXXXXXX 8080
GET / HTTP/1.1
Host: foo
HTTP/1.1 200 OK
...
客户端这个模式是交互式的,输入什么就以原始字节发到服务端端口,效果等同端口转发,但中间没有公网监听。
第三个用法是 SSH。服务端启动免认证 SSH(隧道层有 WireGuard 认证兜底),客户端用 tailcat ssh 连接:
# 机器 A
$ tailcat --serve=no-auth-ssh
# 🐈 Server listening with new address: tcXXXXXXXXX
# 机器 B,进入交互式 shell,或在命令里直接附上要执行的命令
$ tailcat ssh tcXXXXXXXXX
$ tailcat ssh tcXXXXXXXXX ls -la
要复用系统自己的 SSH 服务端(保留原有认证和配置),服务端改成 tailcat --serve=22,把连接代理给本机 sshd 即可。
在此之上有更多子命令和参数:
| 命令 | 用途 |
|---|---|
tailcat(默认) |
stdin/stdout 管道互通 |
--serve=8080,8443 |
把本地 TCP 端口暴露到隧道另一端 |
--serve=no-auth-ssh |
提供 SSH 服务,认证由隧道层的 WireGuard 密钥承担 |
tailcat ssh <token> [cmd] |
作为 SSH 客户端连接 |
--serve=exit-node |
客户端借用服务端的网络出口 |
tailcat socks <token> curl ... |
起一个 SOCKS5 代理 |
tailcat ping --until-direct |
测连通性,显示当前走中继还是直连 |
tailcat parse / resolve |
离线解析 token / 换成自包含形态 |
--allow=nodekey:xxx |
客户端公钥白名单,列表外的握手直接忽略 |
几个模式的机制值得展开。
--serve 暴露端口时,客户端拿到 token 后通过隧道直接访问服务端指定的本地端口,效果等同端口转发,但中间没有公网监听,也不需要在防火墙上开口子。
no-auth-ssh 的“免认证”听上去危险,实际上认证发生在更底层。WireGuard 握手要求双方密钥匹配,能通过握手到达 SSH 服务的只有持有 token 并完成密钥协商的对端,SSH 层的密码认证被隧道层替代了。
exit-node 把服务端当成出口网关,客户端的出网流量从服务端所在的网络出去,IP 归属也随之改变。临时借用某个地区的网络环境时用得上。
--allow 白名单是访问控制的全部。没有它时,任何拿到 token 的机器都能发起连接;配上之后,公钥不匹配的握手在服务端被静默丢弃,对方连服务器是否存在都探测不到。
tailcat ping 的输出直接展示连接质量,每一跳都标明路径:
$ tailcat ping --until-direct <token>
pong in 42.1ms via DERP(sfo)
pong in 1.2ms via 203.0.113.7:41641
第一跳走旧金山的中继(42.1ms),打洞成功后第二跳起就是直连(1.2ms)。--until-direct 会持续探测直到出现直连路径,默认 10 秒超时。
有个巧妙的设计是 token 可以直接当 URL 主机名用。SOCKS 代理模式识别 token 形态的主机名并自动拨号,所以 tailcat socks curl http://<token>:8081/ 里 token 参数都可以省掉。限制是 token 大小写敏感,浏览器会把主机名转成小写,这条路径只在 curl 和多数命令行工具里可用。
密钥生命周期
默认 ephemeral key(临时密钥),每次运行生成新密钥,进程退出地址即作废,适合一次性任务。tailcat genkey 生成持久密钥,存到 ~/.config/tailcat/keys/ 下,地址长期稳定。
这里有个要留意的默认行为。default 是一个魔法密钥名,一旦这个文件存在,之后裸跑 tailcat 会静默改用持久密钥,不再生成一次性地址。区分方式看启动输出,“listening with saved key” 是复用旧地址,“listening with new address” 是新生成的。想强制一次性密钥用 --key=new,用别的密钥用 --key=<name>,删掉默认密钥用 genkey --delete --key=default,genkey --list 列出所有已存密钥。本地存过密钥的机器上跑一次性任务,记得检查这一行。
一个完整的固定域名 SSH 方案
把前面的机制组合起来,就是官方 README 里给出的 “Protected SSH over DNS” 方案,全程零开放入站端口、零端口转发:
- 客户端生成身份密钥对,拿到自己的公钥:
tailcat genkey --client - 服务端生成绑定固定区域的密钥并启动 SSH 服务,白名单只放行这个公钥:
tailcat genkey --fixed-region,然后tailcat --serve=22 --allow=nodekey:cfb6bf... - 把服务端 token 发布成 DNS TXT 记录,形如
my-server.example.com. 300 IN TXT "tailcat=tcXXX..." - 客户端一条命令连接:
tailcat ssh my-server.example.com
--fixed-region 的作用是在 genkey 时刻探测一次最近的中继区域并固化进密钥文件,服务重启后 token 不变,发布在 DNS 里的记录保持有效。默认的 --region=auto 每次启动重新探测,适合一次性场景,不适合发布出去的地址。这个方案里 SSH 服务在公网上完全不可见,端口扫描看不到它,WireGuard 在 SSH 服务看到任何数据包之前就完成了客户端认证,白名单之外的所有握手被静默丢弃。
作为 Go 库嵌入
Tailcat 的代码组织成一个普通 Go module,CLI 只是它的一个使用者。Server 和 Client 两个类型的零值即可工作,隧道在第一次拨号时才建立:
s := &tailcat.Server{
OnTCP: func(port uint16) func(net.Conn) {
return func(c net.Conn) { /* 处理到来的连接 */ }
},
}
s.Start()
fmt.Println(s.ConnBlob()) // 打印连接 token
客户端侧两行完成拨号:
cl := tailcat.NewClient(tailcat.ConnBlob(os.Args[1]))
c, err := cl.DialTCPPort(context.Background(), 80)
这个形态把“打洞加加密”封装成可嵌入的库能力,应用自己决定 token 的分发方式和连接的生命周期。远程开发工具、agent 执行框架这类需要临时打通网络的产品,可以直接把 Server 嵌进自己的进程,跳过 CLI 和管道。前面对比表里 iroh 一行“库形态与工具形态”的差异,指的就是这个维度。
安全与信任模型
官方文档明确说明 Tailcat 不是完全匿名的服务。身份就是“持有 token 加公钥对”,没有用户体系,托管中继会记录连接 metadata,官方对隐私场景的原话是 “isn’t intended for privacy”。
使用时有三条边界要清楚:
- token 等同密码。 泄露即授权,官方警告不要提交进 git、不要公开发布
- 托管中继有日志。 中间人看不到内容(流量 WireGuard 端到端加密),但中继运营方知道谁在何时与谁建立过连接
- 无稳定性承诺。 README 的 Stability 一节写得很直白,Go API、CLI 参数与输出、wire format 都可能变,官方中继无 uptime SLA 和吞吐承诺,且“可能以任何理由随时收回访问权限”
对信任敏感的部署,genkey --region=derp.example.com 可以把自建 derper 的主机名直接写进 token,客户端从此完全不接触 Tailscale 的 DERP map 服务器和中继,限速策略也换成了自己的。自建 derper 只需要一台有域名和 TLS 证书的机器(derper 自己能通过 Let’s Encrypt 签证书),是 Tailscale 长期维护的开源组件。跑整个中继集群的话,用 --derpmap-url 让双方指向自己的 DERP map JSON 即可。这些都不想维护,Tailscale 也提供付费的专用中继和技术支持,README 里那句 “contact sales” 后面跟着的措辞照例带着这个团队一贯的玩笑味。
与同类工具的对比
Hacker News 上 tptacek 的评价是 “Magic Wormhole but for generalized connectivity”,一句话点出了它和文件传输工具的分界。
| 工具 | 与 Tailcat 的差异 |
|---|---|
| iroh / dumbpipe | 功能最接近的 Rust 实现,打洞算法参考了 Tailscale,差异在库形态与工具形态 |
| wush(coder 出品) | 底层同样基于 Tailscale,功能是 Tailcat 的子集 |
| Magic Wormhole | 短人类可读码加 PAKE(口令认证密钥交换)做文件传输,覆盖不了通用连接 |
| 裸 WireGuard | 缺 NAT 打洞,Tailcat 相对它的增量主要就在这一层 |
| Tailscale 本体 | 保留控制平面,可审计、可治理,适合长期组网 |
裸 WireGuard 那一行值得引一句 Brad Fitzpatrick 在讨论帖里的原话:“WireGuard doesn’t do NAT traversal. That’s the main thing this adds.” 两端都有公网可达性时 WireGuard 更简单,两端都藏在 NAT 后时 Tailcat 的打洞层就是全部增量。
iroh 是 Rust 生态里功能最接近的项目,同样提供不依赖控制平面的点对点连接,disco 打洞算法本身就参考了 Tailscale 的实现。两者的差别在生态位,iroh 主打嵌入 Rust 应用的库场景,Tailcat 同时提供开箱即用的 CLI 和 Go 库,命令行侧的完成度更高。
两者的分界线官方给得很直接,需要可治理的网络(身份、策略、审计)用 Tailscale,需要一条快速连接用 Tailcat,一个管长期组网,一个管临时连接。
社区讨论里有两个主要争议。一是 netcat 类比是否恰当,有评论认为 netcat 没有第三方依赖,Tailcat 依赖中继集群;回应是 DERP 本身是 Tailscale 运营 6 年以上的公共品,Tailcat 用的是独立集群,且中继只在牵线和兜底时参与。二是恶意软件可能拿它做 C2 通道(受控端回连指挥服务器的通道),官方中继的限速是主要缓解手段,自建 derper 的运营方则要自己面对这个问题。
适用场景与局限
官方点名的场景都带临时属性:给开发环境开一小时 SSH 访问、让 agent 连一次测试机、传一批文件、联机游戏、CI 任务跳板。共性是连接生命周期短,事后不需要维持关系,用完即弃。
agent 场景被官方摆在很靠前的位置,README 里有个细节能看出重视程度。完整的用法文档被编译进了二进制,tailcat --readme 可以在无网络的机器上直接查看,对应 commit message 写明是为了“只有二进制的人和 AI agent 不联网也能学会用法”。产品页给 agent 留的说明也很直白,agent 走不完注册和登录流程,给它一个 token 就够了,任务结束地址作废,不需要事后清理设备或回收权限。
局限同样明确:
- 连接质量依赖打洞结果,直连失败时官方中继限速,不适合大流量长期转发
- 没有身份和策略体系,多团队、多设备的权限治理做不了,这是剥掉控制平面的代价
- 项目处于早期,生产环境依赖要么自建 derper,要么接受官方中继随时可收回的现状
上手路径
发布工程走 GoReleaser,tag 推送后自动构建发布到 GitHub Releases。Linux 提供 tar.gz、deb、rpm 三种格式,覆盖 amd64、arm64、armv7 三种架构;Windows 提供 zip,覆盖 amd64 和 arm64。全部是静态二进制,没有 Go 工具链也能直接运行,tailcat --version 确认版本。
Docker 镜像发布在 ghcr.io/tailscale/tailcat,多架构 manifest,基础镜像用 distroless static,以非 root 用户运行,自带连接 DERP 中继所需的 CA 证书。macOS 暂时没有预编译包,通过 go install github.com/tailscale/tailcat/cmd/tailcat@latest 安装,Homebrew 支持在计划中。
装好后从上面「基本使用方法」的管道互通开始验证即可。连通性质量用 tailcat ping --until-direct <token> 查看,输出会显示当前连接走中继还是直连,对评估实际带宽有参考价值。
选型判断
三个问题帮你在 Tailcat 和其他方案之间做选择:
- 连接是一次性的还是长期的? 一次性用 Tailcat,ephemeral key 用完即废。长期在线的设备组网用 Tailscale 或裸 WireGuard
- 两端有没有一方具备公网可达性? 有且流量敏感,裸 WireGuard 更简单。两端都藏在 NAT 后,Tailcat 的打洞是直接收益
- 能不能接受托管中继看到连接 metadata? 不能就自建 derper,把 token 里的区域指向自己的服务器
参考来源
作者: Cyber Herald
原文地址: https://torchtree.com/post/tailcat-tailscale-netcat/
发布时间: 2026-08-30
版权声明: CC BY-NC-SA 4.0