使用 systemd-nspawn 或 systemd-chroot 需要关注的地方

原创
2020/05/04 18:27
阅读数 1.4K

本来想写一个如何通过 systemd-nspawn 或 systemd-chroot 使用创建好的映像文件, 但
觉得会写很多内容, 所以这里只写一些自己曾经遇到过的问题, 或者需要特别留意的地方.
阅读这篇文章需要对 systemd-nspawn 或 systemd-chroot 的使用, 有一定了解. 如果有
不明白的地方, 还是请多多 man -k systemd.

另外, 虽然下面所说的内容, 当时有相关记录. 当不保证当时记录足够完整, 以及由
于版本不断变化, 遇过到的问题, 可能现在再也不会遇到.
同时这篇文章, 也不包含相关的配置文件以及相关辅助脚本.

systemd-nspawn 部分

systemd 自带的一种容器方案, 隶属于 systemd-container 包.

1. 容器启动

它支持命令行, .service 文件, .nspawn 文件启动容器. 我自己用的最多的是第二种, 第
一种的话, 不带 --boot 参数启动是调用包管理器做更新, 带 --boot 参数的则是偶尔用
于调试. 第三种几乎没有用过.

2. 容器启动源选择

启动源有两种 image 和 directory. 本来自己选的是 image, 因为很方便. 但是后来在
结合 systemd 的 sandbox(man systemd.exec) 后, 麻烦就来了. 我用到了几个与
sandbox 相关的参数, 其中 InaccessiblePahts= 和 ReadWritePaths= 两个参数, 要求
对应的路径必须存在, 而通过 image 启动, 会产生一个标记文件, 但问题是标记文件不是
一开始就存在的, 自己应该是先 touch 一个, 但结果应该是不行的. 这部分没有详细记录
. 所以后来改为 --directory 来启动容器.

3. 容器登录

machinectl login/machine shell

登录. 后者用的多, 一般是用来以特定用户来运行软件. 第一种用的不多, 一般不会本来
在 Linux 系统里又启动一个 Linux. 不过在 Fedora 系统下, 第一种方式登录, 可能会遇
到类似如下提示

Failed to get machine PTY: Access denied

这个问题与 SELinux 有关, 不建议直接禁用 SELinux, 可以有针对性的调整相应的
selinux module, 以及参考如下链接

https github.com systemd systemd issues 685

对于 debian 系统来说, 要使用 machinectl login, 需要在容器内安装 dbus.
或者

machinectl shell root@debian.image /bin/bash

4. 容器网络

由于 systemd-nspawn 和 systemd-chroot 默认都是直接使用宿主系统的网络, 所以这部
分, 没有特别需要调整的地方.

5. 容器内用户创建规则

因为容器具有隔离性, 所以自己一开始是创建的和宿主系统内的用户拥有相同的 UID 和
GID. 不过这个用户不在 sudo 组里, 或者说没有 sudo 权限. 名字到不一样.

6. 容器内 OS 的计算机名

起了一个容易识别且与宿主系统区分开的计算机名.

7. 图形部分

因为目前 wayland 还不够成熟, 所以用的是 X11. 网上关于如何在 Dokcer 下运行图形界
面程序的教程, 大多采用绑定 X11 socket 文件的方法, 但自己没有采用, 直接给要启动
的程序, 添加 DISPLAY=':0' 环境变量.

如果容器用户和宿主系统用户的 UID 相同, 可以直接添加 DISPLAY. 如果两者不一样的话,

会多一个授权操作, 把和容器内用户相同 UID 的宿主系统用户通过 xhost 添加到信任列表里.

例如,宿主和容器都存在用户 egret(UID 2015), 希望 egret 在宿主系统用户 tom 下运行 GUI 程序.

则在 tom 用户下执行命令

xhost +SI:localuser:egret

当然自己知道这其中存在风险. 这是一位用户对此采取的限制方案

https github.com mviereck x11docker

8. 容器映像文件大小的确定

我按照程序和数据分离的原则, 并结合 systemd-nspawn 支持的可读写 --bind 参数, 和
只读 --bind-ro 参数, 在映像文件里只存放程序文件, 或者用于后续挂载的空目录, 而
数据放在单独的一个位置, 这个位置可以是另一个映像文件, 也可以是宿主系统的一个目
录. 我一般是直接放在宿主系统目录里, 因为不是太好计算要分多大才够用. 但是如果是
希望把文件放在 smb 共享里, 由于文件系统技术限制, 最好还是用映像文件更好.
至于用于存放映像文件的大小, 个人建议一般有 6GiB 到 8GiB 比较适合.

9. 容器内的音频输出

音频输出自己用了几个方法, 一个是针对 pulseaudio. 参考了一些网上教程后, 自己用了
一个如下方法

a. 在容器安装 pulseaudio 包, 并建立一个目录 /audio.
b. 然后在宿主系统安装 socat, 并编写一个脚本.
c. 上面提到脚本, 要做这些事. cp ~/.config/pulse/cookie 到容器的 /audio 目录, 从
   自己后面的观察, 也可以考虑用 bind 方式. 然后用类似如下命令

   socat TCP-LISTEN:5000,bind=127.0.0.1,reuseaddr,fork,range=127.0.0.0/8 \
         UNIX-CLIENT:/run/user/UID/pulse/native

   把 UNIX 套接字转换为 TCP.
d. 在容器内特定用户的 .bashr 或者启动程序脚本, 添加

export PULSE_COOKIE='/audio/cookie'
export PULSE_SERVER='tcp:localhost:5000'

  这利用了容器和宿主系统处于同一网络.

另一个方法, 通过 setfacl 为 /dev/snd 赋予与容器内用户一样 UID 的 rw 权限, 然后
用 bind /dev/snd 到容器内. 如果 UID 都一样, 则 setfacl 设置 ACL 可以省略.

10. 容器内对显卡的调用

因为自己的是核显, 所以没有针对另外两家公司的方案. 还是用 setfacl 对 /dev/dri 目
录及其里面的文件设置 rw, 然后 bind. 但这个方法存在局限性, 由于 systemd-nspawn
做了 SHM 隔离, 自己也没有找到解决方法, 导致一些 Linux 游戏在容器内运行效果非常
差. 加上用容器会启动 systemd, 以及一系列服务, 但自己只是希望运行某个程序, 感觉
进程启太多, 有的还是以 root 权限运行. 万一里面混入恶意代码, 再来个沙盒逃逸...
所以启用了第二个方案 systemd-chroot.

systemd-chroot 部分

先声明一下, 是没有 systemd-chroot 这个命令, 只是仿照 systemd-nspawn 而已.
systemd 的 chroot 存在于 .service 文件里, 有两个参数

RootDirectory=
RootImage=

(man systemd.exec)

可供使用 chroot.

直接选了最经典的 direcotory, 根本没有试 image.

这里先说一个遇到的不影响使用, 但影响收尾的问题. 因为后面说的有些地方, 因为这个
问题进行了一些调整.

这个文件简单来说, 就是在 image mount 到一个目录后, 然后用 chroot 方式是运行了
软件 8, 9 个小时是后关闭软件, 最后 umount. 但就是这个 unmount 不能成功结束, 从
lsblk 可以看到, 虽然其挂载点, 没有了, 但是代表映像文件的 loopN 依然在.

在我用两个系统上(都是 18.04, 不过内核不同, 一个是默认的 4.15, 一个是 hwe 5.3),
一个用的是 xfs, xfs 遇到这个问题, 是没法 systemd-nspawn --image=, 另一个是 ext4
, 这个倒是可以 systemd-nspawn --image=, 更新也是可以. 但问题在于, 即便重新
mount 后, 软件的版本还是老版本, 除非确实 unmount, 在 mount 后才可以看到改变.
这时只能重启解决. 但每次重启都很麻烦. 想要解决这个方法, 所以想要解决这个问题.

最先想到的是, 有进程没结束. 但不应该, 因为用的 KillMode 是 control-group(man
systemd.kill), 就是防止有进程没结束, 一直挂在那. 也分析还在运行的进程, 没有还在
运行的. 用 lsof 找也是一样的.

然后想到, 会不会是结束时, 没有处理好? 由于我运行了 N 个不同的由 chroot 驱动的软
件, 在全部停止时, 会不会产生了某种竞争冲突? 于是在每个进程结束运行后, 加了一个
sleep 3 延时, 但实际结果看, 也不是这个原因.

接着将注意力转移到 umount 命令上. 添加了针对 loopdevice 文件的 --detach-loop 参
数, 同样也没有效果. 又添加 man 里面提到的环境变量 LIBMOUNT_DEBUG=all, 跟踪关闭
过程. 但是从输出的信息看, 没有发现什么特别的. 用 strace 去跟踪也是同样的.

现在又想到, 会不会是一开始就出问题了. 因为我把启动 chroot 的过程,  分为几个服务
进行. 因为担心存在竞争冲突, 甚至把 mount image 加了一个互斥判断. 当然也包括启动
软件前的 sleep 延时. 但依然没有解决这个问题.

目前唯一知道的是, 只要一个软件运行的时间, 有 8, 9 个小时, 那么在结束运行后,
umount 不能完成的可能性就非常高. 现在只是解决了因为这个问题导致不能更新软件的问
题.   

下面开始进入正题.

1. 首先在宿主系统创建特定用户. 本来一开始自己沿用原来 systemd-naspawn 方案里,
   直接使用与宿主系统用户相同 UID/GID 的策略, 但是发现经由 chroot 驱动的软件,
   可以直接 kill 掉具有相同 UID/GID 的宿主系统进程后, 改为新建一个单独的用户,
   名为 egret, UID/GID 为 2015. 并将其加入到适当的组里, 比如 video, 但不绝对包
   括 sudo 这类组.

2. 用 systemd-nspawn 进入到映像文件, 也创建相同的 egret 用, 但是不用也将其加入
   到与宿主系统相同的组里, 因为一个是, 不同发行版, 同一功能的组其 GID 未必相同,
   甚至存在冲突, 二经过实际测试, 我发现只要宿主系统相同 UID 的用户加入到特定组
   后, 容器内的同用户就可以直接访问. 这个发现大大减少了工作量, 看起来也舒服.

3. 务必使得基于 chroot 驱动的软件目录层级清晰明了. 为了便于理解, 这里主要贴树形
   "图", 然后辅以说明.

/entry
|
|--arch
    |
    |--apps
    |   |--ro
    |   |--rw
    |   |--misc
    |
    |--gterminal
       |--ro
       |--rw
       |--user
       |--tmp
       |--misc

    其中 /entry 是主入口. 所有由 chroot 的软件相关目录都挂在其下.

    arch 代表这是 Arch Linux 系统.

    apps 代表 arch 的 image 映像文件, 挂到其下. 在没有意识到存在上面描述的问题
    时, 我是直接以 ro 方式挂载在 ro 目录, 要更新时, 关闭所有软件, 然后通过
    systemd-nspawn 来加载映像文件进行更新. 几经尝试依然无法解决这个问题后, 换用
    先以 rw 方式挂载到 rw 目录, 然后用

    mount /entry/arch/apps/rw /entry/arch/apps/ro --options=bind,ro,private

    以 ro 方式挂载到 ro 目录, privte 表示如果存在新的挂载, 其不会传播到已有挂载
    上, 可参见 mant mount, "Shared subtree operations" 一节. misc 用于后续可能
    存在, 且作用于所有软件的目录或文件的 bind 操作. 用这个方法可以在线更新

    systemd-nspawn --dirctory=/entry/arch/apps/rw

    更新完成后, 当然需要重新开启一下存在更新的软件, 以使得其能运行最新版.

    gterminal 代表 gnome terminal, 因为名字较长, 所以进行了省略.
    使用上面提到 mount --bind 参数, 将 /entry/arch/apps/ro 目录 bind 到这个软件
    的 ro 目录.

    这里多说一下, 使用 mount --bind 参数将一个目录链接到同一分区甚至不同分区的
    特定目录的操作, 在 Windows 下称为目录交接点. 交接点和 mount --bind 的最大
    区别在于, 交接点是静态的, 建立好以后可以一直存在. 而后者, 一旦重启, 在访问
    前必须重新建立.

    在 Windows XP/2003 下可以使用 Sysinterals Suite 出品的 junction 命令实现.
    Sysinterals Suite 的下载地址为

    https: docs.microsoft.com en-us sysinternals downloads sysinternals-suite

    不过由于 XP 和 2003 已经早已停止支持, 所以要用的话, 只能在第三方网站去找,
    找到后, 最好查个毒. 而从 Windows Vista 开始, 系统自带命令 mklink /j 可以
    直接创建交接点. 你可以用 dir /al/b/s 命令, 查找当前 Windows 分区已经存在的
    所有交接点.

    rw 目录. 没有使用, 建立这个目录, 纯粹是与 ro 目录有一个对应关系.

    user 目录. 这个目录专门存存放容器内用户 egret 的相关数据.

    tmp 目录. 容器内用户 egret 的临时文件.

    因为 gterminal 是作为单用户运行, 所以我直接将 user 和 tmp 目录的所有者, 设
    置为 egret:egret.

    misc 目录. 专门针对软件可能存在需要某些 bind 而设置的.

    至于 run 目录, 我是以一定规则放在 /run 目录下, 软件运行前, 才创建. 也设置所
    有者为 egret:egret. 但从自己的观察看, 没有什么软件会用到这个目录.
    
    有一点要特别指出的是, 每个软件的 user, tmp, 甚至 misc 其实都不是直接用来存
    放软件所需的文件或目录的. 因为我的主硬盘不大, 所以这些目录是按照类似 /entry
    层级的形式, 放在从硬盘上, 软件运行前使用 bind 到 /entry 目录下对应的目录.
      
    这个目录的树形 "图" 如下

/data
|
|--arch
    |
    |--gterminal
        |--user
        |--tmp
        |--misc


    用这个方法, 首先所有软件都共用一个 filesystem, 虽然与 overylay 相
    比, 看似要笨重些, 但是方便管理和升级. 虽然 systemd-nspawn 也支持 overylay,
    但我还是推荐 systemd-chroot. 而且还有下面其他好处, 能以同一个用户 egret 身
    份运行不同的软件, 而 systemd-nspawn 难以实现这个需求. 即便 chroot 的方式可
    以看到若干敏感目录, 但受限于权限, 也不能做什么. kill 的进程, 也只包括与自己
    相同 UID 的进程. 而且每个软件都有属于自己的 $HOME 目录, 实现数据隔离.
    
    至于这个举例的 gterminal 用来做什么? 由于像 mplayer 这些从 shell 启动的软件
    , 为了能够让它们方便访问相关媒体文件, 而安装. 至于如何访问, 很简单. 在映像
    文件内部的根目录, 建立 /ro, /rw, 或者 /exchange 目录, 然后在宿主系统对应目
    录, 用 setfacl 为 egret 用户设置适当的 ACL 规则. 然后用 bind 方式到容器内的
    目录, 只需要读则绑定到 /ro, 需要交换或写入, 则绑定到 /exchange 或 /rw 目录.

    由于 WINE 这个软件不需要安装内核模块, 所以也可以用上述方法, 可以把不同的
    WINE 应用隔离开来, 上述目录层级, 只需要把 apps 及其同级的 WINE 应用目录,
    放置到 wine 目录下.

 

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部