半年之前,我写过一篇文章《国产系统之如意玲珑》,介绍了国产操作系统中新兴的软件包格式——如意玲珑。玲珑在设计上采用了容器化运行机制,使应用程序运行在隔离的容器环境中,不仅提高了安全性,也有效避免了系统依赖冲突,相比传统的 DEB 包确实具有诸多优势。
然而,事物都是一体两面的,玲珑带来了安全性和依赖隔离的好处,但同时也带来了其它问题。经过这段时间的玲珑应用适配,深刻感受到玲珑的不方便之处。这个假期,我仔细研究了 snap 包管理器,发现 snap 在一些方面的设计理念与机制,很值得借鉴。
文件系统隔离
容器将应用与系统隔离开来,引起应用和系统的割裂。在适配浏览器应用时,经常就会出现由于文件访问导致的问题。现在鸿蒙、Mac OS 之类的系统都追求多终端融合,一个应用的数据可以在不同终端之间流转,无缝切换。玲珑作为应用容器,还不涉及到多终端的复杂场景,仅仅在一台计算机上,如果还存在障碍,无疑会给用户体验大打折扣。
站在玲珑应用视角所看到的文件系统和宿主机的文件系统是不同的。玲珑采用 docker 那种目录映射的方式来解决文件系统隔离的问题,但这种方式存在两难。
如果将宿主的所有目录都映射到玲珑容器,那就违背了玲珑设计的初衷。所以目前采取的策略是映射有限的几个目录,比如 $HOME,但这会导致容器内的应用存在某些目录的文件无法访问的问题。
比如说,浏览器是支持文件的上传下载的,我们无法控制用户选择哪里的文件。由于浏览器在打开文件选择对话框,这时的视角是宿主机的视角,比如我经常会将工作硬盘挂载在 /work 目录,如果在浏览器中选择 /work 下的文件上传,在容器内 /work 目录是不存在的,这样就会造成文件上传失败。
如何解决这个问题呢?一种方法是改造文件选择器,让文件选择器在容器内打开,这样用户选择文件时,看到的就是容器内的文件系统,而不是宿主机的文件系统。但这样改造会非常麻烦,和虚拟机不同,容器内并没有一个完整的操作系统,而是一个轻量级的操作系统,诸如图形窗口之类的操作还有赖宿主机来实现。
研究了一下 Ubuntu 的 snap 包管理器,snap 采取了 XDG Desktop Portal 机制——也就是常说的 “Portal”(比如 xdg-desktop-portal-gtk、xdg-desktop-portal-kde 等)。
Portal 是什么?
Portal 本质上是一个运行在宿主桌面环境(不在沙箱内)的服务。
当 Snap 应用(比如 Chromium)需要弹出“打开文件”对话框时,它不是直接在沙箱内访问文件系统;而是调用桌面 Portal 服务,请求一个由宿主环境提供的、受控的 Widget。
用户在 Portal 打开的对话框里浏览宿主机的文件夹(即便这些目录并不在 $HOME 下),选中某个文件后,Portal 会把相应的文件描述符(或临时复制的路径)回传给 Snap 应用,让它得以读取。
简单理解:
Chromium(Snap)向 Portal 说:“我要打开一个文件——请给我文件选择对话框。”
portal 在宿主机上弹出一个 GTK/KDE 原生的 chooser,用户在这个原生界面里可以看到完整的文件树(包括 / 下的所有目录)。
用户选了文件,比如 /etc/hosts 或 /data/myfile.txt,Portal 会把该文件以受控的方式(比如把路径绑定到一个沙箱可以访问的临时 FUSE 挂载)交给 Chromium,Chromium 就可以在沙箱内读写这个特定文件了。
这种机制保证了:
安全性:Snap 本身仍然在严格受控法的沙箱里,无法主动“越权”去枚举某个目录;它只能通过 Portal 拿到宿主机授权的某个文件或文件夹。
灵活性:只要用户在对话框里能看到、能选到那个文件,就能让应用访问,无论它原本在宿主机上在哪个路径。
应用程序并不需要做特别的修改,应用程序代码调用 GTK/QT 的标准文件选择 API。在 Snap 环境下,GTK/QT 的文件选择又被自动重定向到 XDG Desktop Portal。Portal 会在宿主机上弹出对话框,授权后把用户选定的文件路径交给应用程序。
不过,这种方案只能解决部分问题,只能针对文件上传下载的场景。比如,系统中的证书等文件,一般是存在在宿主系统的 /etc 目录下,代码中会直接访问一个约定俗成的地址,并不会出现文件选择器。如果容器没有映射 /etc 目录,那么这个文件就无法访问。这个时候需要将系统文件目录映射到容器内,但这又违背了容器化的初衷。snap 又是如何解决的呢?
这就是声明式权限机制。也就是说我可以开放权限,让应用程序访问宿主机的文件系统,但这个权限需要让审核者知道,也需要用户知道。
声明式权限
容器运行应用程序,主打就是安全,但应用程序不可避免的会访问网络、摄像头、系统目录、Removable Media(可移动存储)、系统日志、DBus 服务等。
为了在“自包含 + 安全沙箱”之间取得平衡,Snap 引入了声明式权限(声明式接口,Interfaces)机制,包括 Plug(插口)和 Slot(插槽)。这种权限模型是 Snap 安全沙箱(基于 AppArmor、Seccomp、namespace 等技术)能够灵活、细粒度地控制应用访问主机资源的核心。
Interface、Plug 与 Slot 的基本概念
Interface(接口)
Interface 代表一种宿主机资源或功能点,例如网络访问、摄像头、系统目录、Removable Media(可移动存储)、系统日志、DBus 服务等。典型接口包括:
home:访问用户家目录下除隐藏目录之外的文件。
removable-media:访问挂载在 /media、/mnt 下的可移动存储设备。
network:允许应用与外部网络通信。
camera、audio-playback、wayland、x11:分别对应摄像头、音频播放、Wayland/X11 图形等接口。
system-files、network-control 等:为特殊需求提供对系统文件或网络配置的访问。
Plug(插口)
在 snapcraft.yaml 中,开发者通过 Plug 声明应用需要“插入”的接口,例如:
plugs:
- home
- network
- removable-media
- camera
表示当前 Snap 需要访问家目录、网络、可移动存储以及摄像头。Snap 构建完成后,这些 Plug 会作为该应用可请求的权限列表。
Slot(插槽)
Slot 是“对外提供”的接口,一般由系统或者其他 Snap 包提供。例如,系统会自动为 home、network、removable-media 等接口提供对应的 Slot;某些 Snap 包也可以自己定义 Slot,将自身的一项功能或资源“提供”给其他 Snap 使用(典型场景如:后端服务 Snap 提供数据库连接 Slot,其他 Snap 通过 Plug 来消费它)。
连接(Connect)与断开(Disconnect)
当 Snap 安装后,Plug 并不会自动和系统 Slot 连接。管理员需要手动或者在代码里触发“连接”操作,示例命令如下:
sudo snap connect <snap-name>:<plug-name> :<slot-name>
例如:
sudo snap connect myapp:removable-media :removable-media
完成后,myapp 就能访问系统挂载点 /mnt、/media 下的设备。断开方式类似:
sudo snap disconnect myapp:removable-media :removable-media
这里设计成需要手动运行命令来连接和断开连接,有些对用户不太友好。应用程序可以做些改进,比如在访问受限资源时,弹出一个授权窗口,就像安卓应用那样,在访问相册、摄像头、位置等的时候,会出现一个授权窗口。
声明式权限带来的好处
最小权限原则
通过 Plug 声明,Snap 应用只会获得运行时真正需要的最小权限。若开发者忘记声明某个接口,应用在尝试访问时会被 AppArmor/namespace 阻止,大幅降低潜在恶意或误用风险。例如,一个纯粹播放音频的播放器,只需声明 audio-playback,而不必获取 camera、network-control 等不必要接口。
动态连接与审计可见性
在安装之后,系统管理员可以通过 snap connections 查看当前 Snap 已声明的 Plug 及其对应连接状态:
snap connections myapp
这使得应用权限一目了然,如果某个接口由于安全或资源限制不想让应用继续使用,可以随时 snap disconnect 来断开。所有接口连接操作都有日志记录,方便审计和排查安全事件。
隔离与资源共享两不误
当多个 Snap 需要共享大型运行时(如 GTK、Qt 库)时,可以借助 Base Snap(基础运行时),并通过 Plug/Slot 机制共享接口。例如,一个 Snap 声明依赖 gtk-common-themes,系统会将该基础运行时文件系统挂载到应用的沙箱里,实现了依赖共享、减小整体磁盘占用的效果。与此同时,如果有某个定制化的 Base Snap,第三方应用也能通过 Plug 访问,从而兼顾隔离与资源复用。
Snap 的 Plug/Slot 机制有些类似于 Qt 的信号槽系统,它在服务提供者与服务消费者之间建立了一种松耦合的连接关系。容器化应用在运行时彼此隔离,互不可见,这虽然带来了安全性,但也使得应用间的协作变得异常困难。而通过 Plug/Slot 机制,一个 Snap 应用可以通过声明 Slot 来对外提供某项服务,而其他需要该服务的应用则可以通过 Plug 接入,从而实现跨应用的通信与协作,打破了容器边界带来的交流壁垒。
这种细粒度的权限控制对普通应用程序来说非常必要,它限制了应用的能力范围,确保了系统的整体安全性。然而,对于某些系统级应用——例如系统设置工具、应用管理器等——它们本身的职责就是与系统和其他应用进行深度交互。如果仍将它们限制在严格的容器之中,许多关键功能将无法实现。当前这类系统应用仍主要通过 deb 包部署和安装。
那么,如果要实现这类系统应用的容器化呢?Snap 给出了一种可能的解法——约束模式(Confinement Modes)。
约束模式
Snap 支持三种不同的约束模式,用于定义应用在容器中的权限级别:
strict(严格模式) 这是 Snap 的默认模式。应用在强隔离的容器中运行,只能通过明确定义的接口(Plug/Slot)访问外部资源。适合绝大多数普通桌面和命令行应用。系统资源访问受到严格限制,提高了安全性。
classic(经典模式) 在这种模式下,Snap 应用的权限与传统 .deb 包类似,不再受 confinement 限制,能够自由访问宿主系统的文件系统和进程空间。
devmode(开发者模式) 主要用于开发和调试阶段。应用在此模式下运行时会记录所有越权行为,但并不真正阻止它们。这有助于开发者了解应用在 strict 模式下会遇到哪些限制,并据此进行调整。它不适合用于生产环境部署。
值得一提的是 devmode,对于开发者非常有用。在开发玲珑应用时,经常还需要借助 deb 包来排除问题,因为玲珑并没有提供这种开发者模式。碰到问题,需要对比 deb 包,判断时程序本身的问题,还是由于玲珑环境的限制导致的问题。
snap 的 devmode 主要通过以下几种方式体现:
使用 --devmode 构建和安装 Snap 包。
允许 Snap 以近似“非沙盒”的方式运行。
自动连接所有 plugs 接口,无需手动审核。
对于 Snap 应用开发者,--devmode(开发者模式)带来以下好处:
1. 快速迭代测试
不必担心接口权限、沙箱隔离的问题。
能立即看到应用运行效果,而无需调整 plugs 与接口连接。
2. 更快调试权限问题
在 strict 模式下,如果某个资源访问失败,需要排查 audit.log 日志或使用 snappy-debug。
而在 devmode 下,大多数操作都不会被限制,有助于确认问题是否由 confinement 引起。
3. 提升开发效率
应用在开发者模式下运行时会记录所有越权行为,但并不真正阻止它们。这有助于开发者了解应用在 strict 模式下会遇到哪些限制,及时调整,而不是部署到生产环境,发现有限制后,再来调整。
小结
玲珑作为国产操作系统生态中的重要组成部分,在安全性和依赖隔离方面迈出了关键一步。然而,理想与现实之间,仍有许多细节亟待完善。从文件访问到权限控制,从调试流程到资源共享,Snap 提供了一个相对成熟的模型,其背后的设计理念值得我们深入学习。
玲珑的潜力不可否认,但若希望成为真正通用、可持续的软件分发机制,就必须从用户体验、开发便利性、安全控制等多个维度持续打磨。在这场国产生态建设的长跑中,或许“独创”与“借鉴”并不冲突,关键是以用户为中心,以实际问题为导向,走出一条务实且高效的进化之路。