连接你的首个设备只需五分钟或更短时间 提供超过100种集成 立即开始 太棒了! 看看 IBM 如何将效率提高了 30%! 关闭 即将举行的网络研讨会:替换您的传统 VPN,立即注册

产品 解决方案 企业版客户案例文档博客定价 下载登录联系销售免费开始使用! 产品 了解 Tailscale

探索

解决方案 按用例

按角色

企业版客户案例 导航标题

文档博客定价下载联系销售免费开始使用!登录 WireGuard 是 Jason A. Donenfeld 的注册商标。 服务条款隐私政策 © 2025 Tailscale Inc. 保留所有权利。Tailscale 是 Tailscale Inc. 的注册商标。 博客|见解 2025年4月2日

将 Tailscale 移植到 Plan 9

Plan 9 上的 Tailscale 缩略图 人们常说,没有什么比解释笑话更能让笑话落地了,所以我们在这里解释昨天发布的 Tailscale Plan 9 公告,即使冒着扼杀笑话的风险。 但说真的,如果必须通过解释来扼杀一个笑话,那么没有什么比公司愚人节帖子更适合扼杀的了。 它们通常都很糟糕。 我认为,如果要讲这种笑话,最好付出一些努力; 它 应该 确实 工作。(否则,它就 100% 悲伤,而不是昨天那篇文章中悲伤的某个百分比。)

为了在 4 月 2 日超级明确这一点,因为 4 月 1 日没有人相信任何事情:Tailscale 现在实际上可以在 Plan 9 上运行了。 真的。

我们很高兴地发现每个人都震惊于昨天的博客文章附带了一个 PR,所以让我们深入研究一下该 PR 以及其他已完成的工作。

首先:我真的不了解 Plan 9。 我知道 Plan 9,并且我知道一些了解 Plan 9 的人,但我是一个 Plan 9 新手,如果我的无知冒犯了任何 Plan 9 用户,我提前道歉。 我试图与其他人核实我的无知,以确保它不太愚蠢,但这些帖子中肯定有一些不准确之处,代码中存在错误、捷径和简化的假设。

无论如何。

正如一句俏皮话所说,“我们选择将 Tailscale 移植到 Plan 9 不是因为它很容易,而是因为我们认为它很容易。” 简单来说,看起来你可以采用 Tailscale 的两个 Go 二进制文件,并使用 GOOS=plan9 GOARCH=386 go install ./cmd/tailscale{,d} 构建它们,然后收工。 当然,我预计一些 syscallx/netx/sys/unix 符号在 GOOS=plan9 中不存在,以及一些 //go:build 标签调整和一些 runtime.GOOS == "plan9" 的特殊情况,以使用不同的默认磁盘路径,就像我们之前 为 AIX 所做的那样 等等。 所以这就是我在 2023 年 8 月尝试 做的事情,当时西雅图当地的一位熟人 向我请求 Plan 9 支持,我最终屈服并说好吧(在最初拒绝这个想法之后)。 我调整了一些构建标签和路径并编译了它,然后……Boom。 二进制文件在运行时以奇怪的方式崩溃。 事实证明,Go 编译器对 Plan 9 的支持已经出现了代码腐烂。 Plan 9 不是 Go 的 一流端口 之一,并且没有人注意到回归。 或者,也许 Tailscale 只是比之前在 Plan 9 上更努力地推动了 Go。

无论如何,Tailscale Plan 9 的努力在整个 2024 年都停滞不前,超出了我的时间和/或修复能力。

在 2025 年 3 月初,一位同事提到了愚人节,我突然想起了我们的 Plan 9 端口。

我联系了 Russ Cox(一位来自 Go 团队的前同事,拥有丰富的 Plan 9 历史),并告诉他我认为赶在 4 月 1 日之前完成会很有趣(而且很搞笑)。 他回复说: “当然,我加入。 我们应该修复 plan 9 内核以保存这些寄存器,然后不再有这种特殊情况。”

Russ 可能不知道他要参与什么。

SSE

1999 年,英特尔推出了带有 SSE 指令 的 Pentium III 处理器。 昨天的博客文章的日期是 1999 年,因为这基本上是整个冒险开始的地方。

Russ 上面提到的“特殊情况”是 Go 编译器 试图避免在任何地方使用 SSE 用于 Plan 9 目标,因为 Plan 9 内核不允许在“note handlers”(可以理解为:信号处理程序)中使用 SSE,因为它没有保存/恢复它们。 并且由于 Go 编译器不知道在 note handler 期间使用了哪些代码,因此它保守地尝试在所有地方禁用 SSE。 但是该代码经常被破坏,并且整个编译器中存在太多的 plan9 特殊 情况

理想情况下,Plan 9 内核只需在 note handler 中保存/恢复 SSE 寄存器/上下文,然后 Go 就可以删除 Plan 9 特殊情况并像对待其他操作系统一样对待它。 所以 Russ 做了这件事。

对于 386,Plan 9 的修复是 sys/src/9: allow floating point in note handlers(并在 sys/man/2: update notify 中更新了文档)。

对于 amd64(9k 内核),我们遇到了 更多的问题。 Russ 修复了各种问题:

…… 但到这个时候,我们都几乎放弃了,只是专注于让演示在 GOARCH=386 上运行。

通过相应的 Go 修复 停止(错误地)特殊处理 Plan 9 的代码生成tailscaled 现在可以启动并运行(更长时间)而不会崩溃。 然后我可以开始处理我可以修复的部分。

IPC

现在它由于内存不足错误而崩溃,而不是堆栈损坏。 事实证明,之前尝试将 Tailscale 移植到 Plan 9 时存在一个错误,导致启动了无限数量的 goroutine。 在我们的 safesocket IPC 包中修复了该错误,只是无聊地使用 localhost TCP 修复了该错误,现在 tailscaled 将在没有崩溃的情况下运行。 我认为使用 localhost TCP 不是很 Plan-9-everything-is-a-file-like,但 Russ 指出其他一些 Plan 9 服务也这样做,所以我感觉好了一些,至少可以解除前进的障碍。

稍后使用 Russ 移植到 Gosrv9p 包 并使 LocalAPI 通过它会更好。 或者我们至少应该像在其他平台上那样对 localhost 进行身份验证。 不幸的是,我没有时间。 目前,请勿在共享 Plan 9 机器上使用它。

开发环境

到目前为止,我一直在 VM 中运行 Plan 9,该 VM 是从 9legacy CD 镜像下载 安装的。 因为我不太了解(而且仍然不了解)Acme 编辑器,所以我正在我的普通机器上进行开发并交叉编译 Plan 9 二进制文件,然后在 Plan 9 上运行 hget http://10.0.0.x:8080/tailscaled > tailscaledchmod +x tailscaled./tailscaled 以通过 HTTP 从我的 LAN 中提取二进制文件。 因为我甚至没有将 virtio 用于我的磁盘或网络,所以这个过程(仅仅是通过 LAN 的复制!)每次迭代都需要几分钟。 这足够长的时间让我分心并忘记我正在做什么,并将上下文切换到 Slack 或电子邮件或其他项目。

Russ 或许感觉到了我的痛苦,甚至没有听到我的抱怨,他创建了 https://github.com/rsc/plan9。 这是一个不仅包含 Plan 9 源代码,还包含预编译的二进制文件,以及一个 ./boot/qemu 脚本的存储库,该脚本运行一个无盘 Plan 9 qemu VM,该 VM 通过网络以 9P 根文件系统引导到 localhost 9P 服务器,该服务器从该 git 存储库提供服务。 这意味着不再需要复制文件……我的笔记本电脑的文件系统和我的 Plan 9 文件系统是共享的,就像 Plan 9 应该使用的那样。 此外,作为奖励,qemu 被连接以使用 virtio,使其速度更快。

我现在有了一个很好的开发环境,迭代时间以秒为单位,而不是分钟。

TUN 模式

虽然 Tailscale 现在可以在 Plan 9 上运行并“工作”,但我们只运行 Tailscale 的“用户空间网络”模式,该模式不涉及内核的网络堆栈,而是通过 gVisornetstack 完成所有 TCP/UDP/ICMP 等操作。 这总比没有好,并且我们的 AIX 端口仍然如此,但这并不理想——这意味着从 Plan 9 机器返回到您的 tailnet 的唯一访问是通过 tailscaled HTTP/SOCKS5 代理,并且您必须让您的所有程序都使用该代理。 但是很少或没有 Plan 9 程序识别和支持“HTTP_PROXY”或“ALL_PROXY”环境变量来支持这一点。 也许有一个使用 SOCKS5 的 Plan 9 /net 服务器,但我没有太努力地寻找。

那么,如何让内核参与到网络路径中呢? 在大多数 Unix 平台上,您使用 TUN(或 Windows 上的 wintun),它为您提供了一个虚拟网络设备,您可以在该设备上设置地址和分配路由,并在用户空间中处理传入和传出的数据包。 Plan 9 等效项是微不足道的:您打开 /net/ipifc/clone,读回您刚刚创建的新接口的十进制数字,将 "bind pkt\n" 写入由打开 clone 返回的 ctl 控制 fd,然后您在例如 /net/ipifc/2/* 处有一个新接口,您可以在其中打开 /net/ipifc/2/data 并读取和写入 IP 数据包。 (/net/ipifc/0/1 通常是 localhost 和您的普通物理 LAN)。

当我将此代码发送给 @raggi 进行审查时,他的反应基本上是_“哇,真可爱。 没有 ioctl!”_ 但甚至比没有 ioctl 更美妙的是,对数据文件的读取和写入甚至不需要额外的帧来预先添加长度。 您只需读取和写入 IP 数据包即可。 这实际上是我们在任何平台上拥有的最简单的“TUN”实现。

路由表

我现在可以将数据包发送到我接口的地址,但不能发送到我的 tailnet 中的对等方。

现在我需要实现 Tailscale 的 router 接口。

在 Plan 9 上操作路由表就像创建接口一样容易。 您打开 /net/iproute,将 "tag tail\n" 写入其中以在您在该 fd 上添加的所有未来路由上设置“tail”路由协议标签(以便在我们清理自己后,很容易知道我们添加了什么),然后写入一些小消息,例如 "add 100.64.0.0 /106 100.102.103.104",将其自身的 IP 地址作为下一个跃点值提供。 唯一的意外是那里的 CIDR 长度(“/106”)是 106,而不是您期望从 CGNAT 范围 的 10.64.0.0/10 中获得的 /10。 事实证明(或者看起来),Plan 9 在内部主要是 IPv6,而 IPv4 只是其中的一个特例,因此写入“100.64.0.0”只是编写 IPv4 映射的 IPv6 地址(如 ::ffff:100.64.0.0)的简写方式。

缺少的三键鼠标

此时我进行了一次小旅行,忘记带上我的三键 USB 鼠标。

正如昨天提到的,Plan 9 基本上需要一个三键鼠标才能使用。 这使得在 Mac 笔记本电脑上进行开发非常困难,甚至到了不好玩的程度。

Russ 再次可怜我并 修改了 Plan 9 以支持按住修饰键,同时单击以模拟按钮 2 和按钮 3。

Tailscale SSH

在一时自信(或者无聊地等待我延误的航班回家)的时刻,我决定解决 Tailscale SSH 支持。 Tailscale SSH 是 tailscaled 的内置 SSH 服务器,它通过使用您的 Tailscale 身份(由与您的所有数据包关联的 WireGuard™ 密钥已知)来处理身份验证。

天真地,我尝试使用 os/exec.Command 运行 Plan 9 shell (/bin/rc) 并将其 stdin/stdout 连接到它。

这“奏效”了,但有点糟糕——事物没有正确回显或导航。 你无法中断进程等。

Russ 向我解释了如何正确地做到这一点,但他可能感觉到了它似乎有多么令人难以承受,所以他离开了并向 9fans/go 存储库添加了一个 “netshell”示例。 那个“netshell”基本上是世界上最不安全的 telnet 服务器,但它是我需要放在 Tailscale SSH 后面的东西,而不是直接运行 /bin/rc

现在 SSH 可以工作了。 方便的是,这也意味着我可以更轻松地从 Plan 9 获取文本输出:我可以从我的笔记本电脑(VM 主机)ssh glenda@plan9 cat /dev/snarf,并从我的 Plan 9 访客 VM 获取复制/粘贴缓冲区。 (cat /dev/snarf 就像 macOS pbpaste)当然,这主要是因为我没有思考并且没有意识到我有一个共享文件系统,并且还可以将 /dev/snarf 重定向到一个文件并从我的笔记本电脑读取该文件。 没关系。

但是 Tailscale SSH 也让我更轻松地在我的笔记本电脑上编写 Go 测试,然后轻松地通过 SSH“远程”交叉编译和运行它们。

服务收集

tailscaled 可以选择执行(默认禁用)的一件事是查看您在机器上运行的服务并将其报告给控制平面以进行发现,因此例如您知道您正在端口 8080 上运行一些开发服务及其进程名称。

我很好奇如何做到这一点。 基本上,您只需遍历 /proc/NNN/fd(类似于 Linux)并找到打开例如 /net/tcp/clone 的进程 PID。 然后,您查看它们的“QID”并将其与 /net/tcp/NNN/{status,local} 排成一行,以查看它们是否正在侦听以及在哪个端口上。 总的来说,它与其他平台非常相似,但没有我希望的那么漂亮。 事实上,您必须执行 tcpN := (qid >> 5) & (1<<12 - 1) 才能将 FD 的 QID(基本上是它的 inode 编号?)映射到 TCP 编号并希望内核实现不会改变有点令人难过。 如果我们更改 Plan 9 以明确支持该操作会更好,但我们没有时间了。 没关系。

MagicDNS

接下来是使 MagicDNS 工作。 理想情况下,您只需从 Plan 9 将对等方称为“foo”(例如 ip/ping foo),或者至少使用其 foo.tailnet-name.ts.net FQDN。

ndb(6)ndb(8) 以及 dial(2) 中有很多关于 Plan 9 上名称查找生命周期以及哪些层执行哪些操作的文档。 Go 标准库代码也很容易阅读,以了解它的工作原理,至少从客户端的角度来看。

我们争论只是拦截所有 /net/dns/net/cs 查询并将 Tailscale 名称混合在一起,但最终 Russ 再次修补了 Plan 9 以允许为特定 DNS 名称后缀指定备用 DNS 服务器,类似于 systemd-networkd 在 Linux 上允许的内容

为了获得额外的乐趣,我一直随机遇到一个错误,即使在我将“refresh”写入 /net/dns 后,DNS 查询也错误地进行了负面缓存。 Russ 也修复了它

随机时间崩溃

有时我注意到 tailscaled 从 gVisor 的 netstack 中出现断言崩溃,因为它观察到它的 单调时间 已经倒退了。 单调时间永远不应该倒退; 这是它的唯一工作。 但事实证明,Go 在 Plan 9 上的时间实现只是将挂钟时间用作其单调时间,并且当 ntpd 向后调整时钟时,gVisor 崩溃了。

Russ 再次介入并 将单调时间添加到 Plan 9 的 /dev/bintime修补了 Go 以使用它

博客时间

最后的 Boss 是写一篇博客文章并弄清楚如何再次访问该博客。(对不起,好久不见。我会尝试更频繁地写作!)

我认为将第一篇博客文章的日期定为“1999 年 4 月 1 日”会很有趣,因为这很怀旧。 而且当时的世界似乎更快乐。

我也缠着一些曾在贝尔实验室工作的前 Go 同事进行引用。 我很高兴他们愿意参与进来。 我防御性地向他们保证,Plan 9 不是笑话的对象,而笑话是我们自己……也许? (我不完全确定我们为什么要这样做。)

在 Web 上运行

还剩几天的时间,我们决定解决在 Web 上运行 Plan 9 的问题。 几周前,我首先考虑使用 PCjs 并在我家附近与它的作者共进咖啡和甜甜圈。 但是,如果没有网络支持,演示就没那么有趣了。 添加 ne2000 支持可能是可能的,但是没有很多时间。 Jeff 建议看看 copy/v86。 它使用 WASM 运行 32 位操作系统,并包括 各种形式的网络 支持。(这是我们专注于 GOARCH=386 而不是 GOARCH=amd64 的另一个原因!)

到目前为止,我一直在使用 Russ 准备的 基于 qemu 的共享文件系统环境 进行开发。 但是现在我们需要一个磁盘镜像才能从 Web 上启动。

当 Russ 致力于清理和现代化他 25 年前致力于开发的旧的压缩根文件系统内核时,我致力于探索网络选项。

其中一个网络选项通过 websockets 将第 2 层以太网帧传递到中继。 我 向我们的集成测试使用的网络模拟环境添加了 wsproxy 协议支持。 该环境在 gVisor 的 netstack 的帮助下模拟一切:ARP、DHCP、带有伪造 IP 的 DNS、各种 NAT、可选地将 controlplane 和 DERP 服务器桥接到幕后它们在真实连接上,等等。 这最终奏效了,但这并不理想。 VM 在它启动 rio(Plan 9 GUI)之前执行 DHCP,并且 DHCP 通过伪造的基于 websockets 的以太网进行了多次往返。 如果中继很远,则 GUI 会启动得很慢。

所以然后我实现了 “WISP”服务器。 这使得 GUI 启动时没有任何网络往返:DHCP 全部伪造发生在浏览器内部。

我正忙于生产化 WISP 代理,当时我没有时间了,并决定使用 copy.sh/v86 默认网络中继设置启动。

我构建了 Tailscale,将其添加到 /386/bin,准备了一个磁盘镜像(在将 tailscaletailscaled 添加到镜像的 proto 模板文件之后,在 rsc/plan9cd /sys/lib/dist/mini; mk),然后它吐出一个 16MB 的磁盘镜像,其中包含所有 Plan 9 和 Tailscale,它本身在解压缩后是一个 23MB 的二进制文件。 这就是为什么您会注意到启动镜像时的“gunzip…”步骤,现在包含在 https://copy.sh/v86/?profile=9legacy 的示例镜像中。

也许我稍后会完成 WISP 后端。

未来方向

Plan 9 有两个主要分支:一个非常小的分支 (9legacy) 和一个修改更多的分支 (9front)。 到目前为止,Tailscale 仅在 9legacy 上进行了测试。 Russ 为 9legacy 编写的一些补丁可能仍然需要移植到 9front。

我们还应该验证 64 位 GOARCH=amd64 支持是否有效。 在开发过程中,我们主要忽略了这一点。

我也没有实现出口节点支持或 Go 的 net/netns 包支持。 这样做可能需要重新思考 Tailscale 如何在 Plan 9 上展示自己,可能 作为它自己的 /net

但在很大程度上,如果 Plan 9 社区愿意接管,我将依赖他们。

重点是什么?

我现在正试图记住我们为什么要这样做。 主要是因为 Skip 询问了。 部分原因是它很有趣且具有教育意义,可以在替代现实中工作并学习新事物。 并且使用完全损坏的 tailscaled 以奇怪的方式崩溃和死锁总是似乎