# Wifibox：一种嵌入式虚拟化无线路由器

* 原文链接：[Wifibox: An Embedded Virtualized Wireless Router](https://freebsdfoundation.org/our-work/journal/browser-based-edition/virtualization-2/wifibox-an-embedded-virtualized-wireless-router/)
* 作者：**Gábor Páli**

由于生活优先级的变化，我在 2017 年左右远离了 FreeBSD。后来我重新回归，开始为自己构建一台基于 FreeBSD 的工作站，是一台联想 ThinkPad X220。我注意到，尽管它能够工作，但无线支持仍然远未达到最佳状态，驱动程序 `iwm` 既不稳定，也不适合日常使用。

我意识到，在无线网络领域，FreeBSD 仍然在努力追赶 Linux 系统的性能，原因在于缺乏最新的硬件支持。这背后有一个原因：FreeBSD 常常不是首选目标，因此它不会成为这些开发的目标，而它的网络子系统部分需要提升，以满足最新的要求。这不是一个简单的问题，FreeBSD 基金会正在资助一个长期项目，旨在为该堆栈带来更新，并建立一个框架，便于重用来自 Linux 的无线网卡驱动程序。

这一点让我感到困扰，我不能再耐心等待问题得到解决。我想重新参与 FreeBSD 的开发，因为我不仅喜欢使用它，还喜欢学习它。然而，我没有足够的时间来精通网络和驱动程序代码的开发，因此我需要寻找其他机会。

## 原型

当我了解到另一种方法时，我感到非常兴奋（感谢 Gábor Zahemszky 的提醒！）。这个方法的核心是利用 `bhyve` 的 PCI 直通功能来运行 Linux 虚拟机，借此可以直接与硬件通信，设置无线连接，并将网络共享给 FreeBSD 主机。我发现了 [David Schlachter 的一篇优秀博客文章](https://www.davidschlachter.com/misc/t480-bhyve-wifi-pci-passthrough)，文中详细介绍了整个过程，我也通过这篇文章的帮助成功构建了自己的原型。

在试验整个过程的过程中，我从至少两个角度进行了研究。首先，考虑到系统升级，是否这个方法可以长期维持；其次，考虑到没有深刻理解此解决方案的用户，是否可以轻松安装和移除该方法。作为一名前 Ports 开发者，我知道我必须能够维护这些组件，并使它们与基本系统隔离，同时与其他第三方集成。那么为什么不利用现有的 Ports 框架呢？于是，在 2021 年 4 月诞生了 Port `net/wifibox` 的概念。

最初，我使用 Port `sysutils/vm-bhyve` 来构建和管理基于 [Alpine Linux](https://alpinelinux.org/) 的虚拟机。Alpine 是一款轻量级的 Linux 发行版，采用 OpenRC 作为 init 系统，使用 `musl` 标准 C 库使得能够创建小型应用程序，并集成了 BusyBox 用于最常用的命令行工具。最初，它是作为嵌入式优先的发行版创建的。我在使用 Docker 容器镜像时了解到了它，并记住了它的小巧和易用性。它得到了积极维护，提供了大量通过“aports”系统管理的软件包。回顾过去，整个系统在许多方面与 FreeBSD 类似，我也变得很喜欢它。

尽管 `vm-bhyve` 是一个出色的工具，但我觉得它对于这个特定的用例来说有些过于复杂。相反，我将其作为基本用户界面的模型，例如提供一个控制台让用户与虚拟机进行交互，以及一些基本的编排例程。由于这些例程需要与命令行的 `bhyve` 工具进行交互，我决定坚持使用基于 shell 的方法。如果我尝试用其他更高级的语言（如 Python）实现所有这些操作，可能不会有更好的体验，因为那样会不必要地增加构建时间，并引入对其他第三方软件包的依赖。

最终，Wifibox 的用户界面包括命令 `start`、`stop`、`restart`、`status` 和 `resume` 。恢复操作需要特别处理，因为已知在笔记本电脑睡眠时，虚拟机会失去与虚拟化 PCI 设备的连接，故必须以某种方式恢复这种连接。经过一些实验，我发现通过停止虚拟机、重新加载 `vmm` 内核模块并重新启动虚拟机，可以缓解这一问题。最近，Joshua Rogers 提出了一个[解决方案](https://joshua.hu/brcmfmac-bcm43602-suspension-shutdown-hanging-freeze-linux-freebsd-wifi-bug-pci-passthru)，在内核层面解决了这个问题，但这个修复还没有被加入到基本系统中。

虚拟机与 PCI 设备的交互常常被证明是一个弱点，这通常限制了 Wifibox 本身的可用性。作为这种解决方法的改进，恢复方法的范围已经得到扩展。某些硬件配置对设备关闭和重启的方式有不同的反应。例如，得益于 Joshua 的工作，发现设备是否在虚拟机的关机过程中正确关闭非常重要。还发现，Linux 内核模块 `ath11k_pci` 在虚拟化环境中的运行并不理想，因为它假设消息信号中断（MSI）表的位置与主机一致。只有在 FreeBSD 主机以某种方式支持为虚拟机注入主机物理 MSI 信息或禁用 MSI 虚拟化时，才能处理这个问题。

## VirtFS/9P 支持

Wifibox 的一个主要设计原则是用户无需了解底层虚拟机，就能够将其作为原生应用直接在主机上运行。为了实现这一点，探索了最近完成的 `bhyve` VirtFS/9P 文件系统直通支持的工作。通过在主机上挂载适当的目录到 `bhyve` 虚拟机，所需的配置文件可以导入，日志文件也可以导出。这样，用户就不必手动移动或保持虚拟机与 FreeBSD 主机之间的文件同步。

从 FreeBSD 13 开始有 VirtFS/9P 支持，但我希望将其扩展到当时的旧版本（12 和 11），以扩大其用户群。幸运的是，这个功能包含在一个单独的模块中，我能够创建另一个 Port，名为 `sysutils/bhyve+`，以自动修补基本系统中的 `bhyve` 源代码，使其包含该模块。有了这个，用户不必等待原作者回溯这个功能，但可以根据需要为 Port `net/wifibox` 拉入这个额外的依赖项。除了添加 `virtfs-9p`，`bhyve+` Port 还包含了许多其他修复，使得 Wifibox 可以运行。基本目标是组合一个版本的 `bhyve`，使其在每个主要的 FreeBSD 版本中都相同，最大程度地减少差异。这个版本本来是基于 13.x 线的，但相关的架构变更使得这一点变得不简单，后来这个想法被放弃了。多年来，它的相关性逐渐减弱，最终变得过时。

为虚拟机创建磁盘镜像面临许多挑战。主要问题是，最初的镜像是一个预安装的 Alpine 系统。它在我的工作站上本地维护，难以追踪其包含内容，并且由于写入各种临时和工作文件，它不断变化。从用户的角度来看，这引发了一个有效的问题：是否信任“别人家的虚拟机”。最初的版本大约是 640 MB，与典型的嵌入式系统镜像相比显得很庞大。这个大小部分是由于镜像包含了所有可能有用的工具和文件，因此，下一步的逻辑就是使其更小巧和模块化。

## 版本 1.0

在 2022 年 5 月发布的版本 1.0 中，进行了大量工作来重构镜像的创建过程。主要目标是使整个过程可重复且精简。从技术上讲，安装系统组件的复杂性被转化为 Port 的 `Makefile`。镜像获得了自己的子 Port `net/wifibox-alpine`，而协调脚本被拆分为 `net/wifibox-core`，`net/wifibox` 成为了一个元 Port。通过不断实验 Alpine 安装文件、根文件系统包及其包管理器 `apk` 工具，它们被调整为在 FreeBSD 上运行，并借助 Linux 兼容层实现。此外，为了实现自动化安装，将创建一个 `Makefile`，将系统安装到主机上指定的目录，并可选择扩展额外文件，会在那里下载并安装 Alpine 包。包本身提供了一种模块化构建镜像的方法，并使用户可以通过各种 Port 选项进行选择。例如，可以单独安装各大无线网卡品牌的固件文件，并为它们创建 FreeBSD 包的不同版本。

基于包的方式使得创建额外的包以及修改现有包成为可能。不幸的是，许多上游的 Alpine 包含有一些额外的内容，如文档和附加的二进制文件，因此需要将其删除。但它也使得像 `mDNSResponder` 这样的应用可以被移植到这个平台，并作为解决方案的一部分运行。Linux 内核本身的包必须经过大量编辑，去除所有未使用的组件，减少其资源消耗并缩小攻击面。Wifibox 不需要标准的基于 `initrd` 的启动过程，因此初始的临时根文件系统被完全移除，直接从其根文件系统进行启动。与 AMD64 架构无关的配置文件和补丁被移除，因为 Wifibox 仅支持该特定架构。

虚拟机镜像通过 SquashFS 压缩，保持整体大小在 15 MB 左右。此方法还使用了一个只读的根文件系统，防止即使是 `root` 用户也无法篡改。它通过一个内存支持的临时文件系统扩展，挂载在 `/tmp` 下，管理运行时的文件写入，此外还使用 VirtFS/9P 挂载来读取来自主机的应用配置文件。启动过程通过 GRUB 进行，因此 Linux 内核（没有其模块）不包含在此镜像中，而是通过 `sysutils/grub2-bhyve` 预加载。

## 包框架

为了推出所需的 Wifibox 包，除了导入的上游包外，还采用了 Alpine Linux 的包框架。为了透明性和可重复性，每个自定义包都有自己的 `APKBUILD` 文件和版本控制的额外文件，这些文件存储在 `git` 中。包是在一个干净的、专用的 Alpine Linux `bhyve` 虚拟机上构建的，该虚拟机通常被称为 `wifibox-dev`，并在每次小版本的 Alpine 发布时重新创建。生成的包会上传到 GitHub，供用户方便下载。在过去，也曾尝试在 Linux 的 `chroot` 环境中构建包，但 FreeBSD 原生的 Linux 模拟支持不足以满足这个目的。跨编译的结果也类似，因此我最终也选择使用 `bhyve` 来实现这一点。根据 Bernhard Fröhlich 的建议，我目前正在考虑利用 GitHub Actions 在原生 Linux 环境中自动独立地构建 Wifibox 包。

## 基于 Linux 的无线网络栈

一个常规的基于 Linux 的无线网络栈在虚拟机中运行。首先，Linux 内核用于检测 PCI 无线设备，并通过其驱动程序以及相应的固件（如有必要）使设备运行。由标准的 OpenRC 服务启动无线网络接口 `wlan0`。接着，WPA Supplicant 或 `hostapd` 会连接到该接口以完成配置。除了无线接口外，`bhyve` 还暴露了一个虚拟的 `eth0` 以太网接口。在主机上，定义了一个桥接接口 `wifibox0`，并通过一个 `tap` 软件隧道将其与虚拟机中的 `eth0` 接口连接起来。使用 `ip-tables`，应用了网络地址转换（NAT）和数据包转发，使得流量在 `wlan0` 和 `eth0` 之间双向流动。IP 地址通过 Busybox 内置的 `udhcpd` 获取（在主机上通过 `eth0`），而在虚拟机内（通过 `wlan0`），则使用 `dhcpcd` 或 `udhcpd` 获取 IP 地址。主机的 IP 地址范围可以在 Wifibox 配置中控制，并根据用户需求进行调整。

![](https://freebsdfoundation.org/wp-content/uploads/2025/01/wifibox_architecture.png)

由于引入了 NAT，请注意，Wifibox 为 `eth0` 和 `wlan0` 使用了不同的 IP 地址范围。因此，某些应用程序可能无法直接正常工作，需要部署额外的工具和配置，例如端口转发。从安全角度来看，这可以视为一个优点，因为我们自动安装了防火墙。但同样，这也是一个缺点，因为它打破了端到端的连接性——这是互联网的核心原则。为了解决这个问题，曾进行过实验，将数据包转发推到以太网层级。例如，存在 [WLAN Kabel](https://github.com/escitalopram/wlan_kabel)，它实现了在以太网和无线接口之间移动数据包。虽然流量能够正常流动，但 DHCP 通信无法穿过这个障碍，且观察到的性能较低。尽管如此，这仍然是一个有趣的方案，值得在未来进一步探索。

使用 Wifibox 的一个优势是所谓的“Unix 域套接字透传”，它帮助像 `wpa_cli` 和 `wpa_gui` 这样的工具直接与虚拟机内部运行的 WPA Supplicant 通信，从而用户不必在虚拟机内运行这些工具并与虚拟机交互。这是额外的功能，因为虚拟机文件系统中的 Unix 域套接字并未通过 VirtFS/9P 导出，因此主机无法看到它们。为了解决这个问题，虚拟机中运行了一个专门的 `uds_passthru` 进程，该进程在后台运行精简版的 `socat`，将套接字转换为 TCP 端口。主机一侧有一对进程，它通过这些端口与虚拟机通信，并将数据转回本地的套接字。通过这种方式，Wifibox 可以模拟该套接字的存在，实现平滑的通信。与虚拟机配合，协调脚本通过一个特定的配置文件自动管理套接字透传。

## 作为产品的 Wifibox

虽然 Wifibox 客户操作系统主要基于 Alpine Linux，但它通常包括额外的补丁。例如，它从 Arch Linux 导入了一些补丁和软件包，因为 Arch 对无线设备的支持更好。但也发现，旧款博通网卡的驱动程序根本不支持 MSI，这是虚拟化环境中必须具备的功能。某些驱动程序的初始化延迟过慢，导致 WPA Supplicant 在启动时无法找到 `wlan0` 接口，因此实现了指数退避机制以增强其鲁棒性。正如经验所示，专门解决这些问题的小型操作系统发行版是非常必要的。

协调器脚本使得可以将 `bhyve` 与其他工具结合，进一步塑造其行为。由于 Anton Saietskii 的观察，`nice` 被用于帮助控制负责运行虚拟机的进程的优先级，避免过度加载主机。同样，增加了一个与 `daemon` 配合的层，用于监控状态，如果虚拟机崩溃或被故意重启，则重新启动它。

截至目前，正在积极维护 Wifibox，并且每年三月和九月发布半年度更新。在 Ashish Shukla 的帮助下，这些发布被推送到 FreeBSD Ports，因此可以通过 `pkg` 工具安装。需要注意的是，Wifibox 并未出现在 FreeBSD 发行版的安装介质上，这可能使得在通过无线网络安装 FreeBSD 时，使用 Wifibox 更加困难。然而，可以将所有必需的二进制包添加到安装介质中，并在开始安装过程之前使用它们初始化网络连接。

在项目的 [主页](https://github.com/pgj/freebsd-wifibox) 上，可以找到源代码及相应的 GitHub 仓库的指向，可以打开问题单并开始讨论。还有一个单独的仓库[用于存放 Port](https://github.com/pgj/freebsd-wifibox-port)，发布的开发版本提供了预览下一步进展并测试修复问题的机会。

## 文档

Wifibox 附带了大量的文档，因此我推荐读者进一步研究。我还想强调一个隐含但重要的组织原则，即关于文档的编写。Wifibox 遵循分阶段的“边读边走”模式。这意味着它没有详尽的在线文档，主 GitHub 仓库中的 `README` 文件涵盖了介绍、基本安装说明和已知兼容的硬件配置列表。用户随后必须安装 Wifibox 才能访问手册页，手册页提供了有关如何使用该工具和配置文件位置的详细信息。然后，在配置文件中，提供了更多的说明，指导用户完成相关步骤，同时在遇到错误时提供有用的错误信息以帮助用户。虚拟机镜像有自己的专门手册页。感谢 John Grafton、Warner Losh 及许多其他用户提供反馈，帮助我改进这些文档。

## 总结

这一切是好奇心的产物，旨在为 FreeBSD 无线网络之旅中的挑战提供快速解决方案，并发掘 PCI 直通技术和 `bhyve` 虚拟化设计所带来的技术优势的另一种用法。Wifibox 仍然存在一些缺点，远不能替代原生解决方案。因此，它被称为一种嵌入式虚拟化无线路由器，避免了购买专用硬件的需求，并创造性地将 CPU 的现有虚拟化能力呈现为这种设备的模拟。然而，我相信这一方法仍然有一些值得追求的想法，例如，使用 `bhyve` 只在虚拟机中运行 Linux 内核及其驱动程序，并将其直接作为无线网络设备暴露出来。这将使该方法更加接近本地解决方案，但目前尚不清楚它是否可行，且需要多少工作量。在此期间，我希望 Wifibox 能够减轻将本地解决方案推向生产环境的压力，并向用户保证，FreeBSD 依然是一个出色的选择，他们不必放弃通过无线连接获得良好的速度和可靠连接的可能性。

***

**Gábor Páli** 是位多年来始终愉快且忠诚的 FreeBSD 用户，他也曾在文档编写和 Ports 开发方面获得过乐趣。他与妻子一起住在美丽的匈牙利城市埃斯泰尔戈姆（Esztergom）的边缘。他支持在行业中推广函数式编程，现如今他为 Apache CouchDB 做贡献，编写 Erlang 和 Scala 代码。
