在Docker容器中使用FUSE文件系统

容器使用 FUSE 的问题

我们一般使用的 Docker 容器都是非特权容器,也就是说容器内的 root 用户并不拥有真正的 root 权限,这就导致很多属于系统管理员的操作都被禁用了。

最近有个在 IBM Bluemix 容器内部挂载 FUSE 文件系统的需求,例如我使用 davfs2 挂载 WebDAV 服务器不出意外地会报错:

root@instance-007a20ff:~# mount.davfs https://dav.jianguoyun.com/dav/ /mnt/
Please enter the username to authenticate with server
https://dav.jianguoyun.com/dav/ or hit enter for none.
  Username: xxx@xx.com
Please enter the password to authenticate user fcoe@qq.com with server
https://dav.jianguoyun.com/dav/ or hit enter for none.
  Password: 
mount.davfs: can't open fuse device
mount.davfs: trying coda kernel file system
mount.davfs: no free coda device to mount

mount.davfs 命令报错表示无法打开 fuse 设备,而 fuse 设备实际上是存在的(说明 fuse 模块也已经加载了):

root@instance-007a20ff:~# cat /sys/devices/virtual/misc/fuse/dev
10:229

从容器内部可以查看到 cgroup 实际允许访问的设备,并没有包含 fuse 设备:

root@instance-007a20ff:~# cat /sys/fs/cgroup/devices/devices.list
c 1:5 rwm
c 1:3 rwm
c 1:9 rwm
c 1:8 rwm
c 5:0 rwm
c 5:1 rwm
c *:* m
b *:* m
c 1:7 rwm
c 136:* rwm
c 5:2 rwm
c 10:200 rwm

手工允许 fuse 设备自然也是不可行的:

root@instance-007a20ff:~# echo "c 10:229 rwm" > /sys/fs/cgroup/devices/devices.allow
-bash: /sys/fs/cgroup/devices/devices.allow: Permission denied

另外由于 Bluemix 提供的是非特权容器,即使允许访问 /dev/fuse 设备也会因为没有 mount 权限而挂载失败。

UML 系统修改

虽然 Docker 容器内部不能直接挂载使用 FUSE 文件系统,但我想到如果用 User-mode Linux(以下简称 UML) 来实现在应用层再运行一个 Linux kernel,就可以在 UML guest 系统中挂载 FUSE 文件系统了,而且 UML 系统中也可以通过 hostfs 直接访问容器本身的文件系统。

有关 UML 的介绍和编译使用可以参考我之前写的 小内存OpenVZ VPS使用UML开启BBR 文章。

首先我们需要修改 UML kernel 配置:

  • 增加对 FUSE 的支持,这个是必须的了,否则无法在 UML guest 系统中使用 FUSE 文件系统;
  • 增加了对 xfs、btrfs、ext4、squashfs、iso9660 等常见文件系统的支持(方便在 UML 系统中挂载各种文件系统镜像);
  • 另外为了支持将 UML guest 的文件系统导出,启用了 sunrpc 和 NFS 服务器支持;
  • UML 网络配置中增加了对 Slirp 和 VDE 网卡的支持。

UML kernel 目前支持常见的几种网络模式:

  • TUN/TAP
    最简单和常用的模式,不过 host kernel 需要支持 tun 或者 tap 设备,这个在 Docker 容器中一般都不可用的;
  • SLIP
    SLIP 串行线路 IP 支持,现在一般很少用到了,同样 host kernel 需要支持 slip 设备;
  • Slirp
    通过用户层的 slirp 程序实现 SLIP 连接,好处是不依赖任何内核层的设备,而且 slirp 可以支持非 root 用户使用,不过 Slirp 只支持模拟 IP 协议的数据包,详细可以参考 Slirp 开源项目的官方网站。
  • VDE
    VDE(Virtual Distributed Ethernet)可以在不同的计算机间实现软件定义的以太网络,同样支持在用户层以非 root 用户身份来运行,目前 Linux 上的 QEMU 和 KVM 虚拟机都支持 VDE 虚拟网络。

鉴于我们需要在 Docker 容器中运行 UML 系统,目前只能使用 Slirp 和 VDE 模式的网卡,另外单独的 slirp 程序使用起来相对 VDE 也更简单(支持 VDE 的 UML kernel 可以直接调用 slirp 程序,不像 VDE 还需要预先使用 vde_switch 等命令配置软件交换机),因此这里的 UML 系统就使用 Slirp 网卡了。

当然原来基于 busybox 的 UML 系统用户层也做了些修改:

  • 增加 libfuse 支持挂载 FUSE 文件系统;
  • 增加 rpcbind、nfs-common、nfs-kernel-server 等软件包,支持在 UML 系统中运行 NFS 服务器,导出 UML guest 的文件系统;
  • 增加 dropbear,支持 SSH 和 SFTP 服务器和客户端了;
  • 增加 httpfs2 FUSE 文件系统支持,来自 GitHub 上的 httpfs2-enhanced 项目,方便挂载访问 HTTP 和 HTTPS 远程文件;
  • 增加 archivemount FUSE 文件系统支持,方便直接挂载 zip、rar、tar.gz 等各种格式的压缩包以支持直接访问压缩包中的文件;
  • 增加 CurlFtpFs FUSE 文件系统支持,支持挂载 FTP 远程文件;
  • 增加 davfs2 FUSE 文件系统支持,支持挂载远程 WebDAV 目录;
  • 增加 sshfs FUSE 文件系统支持,支持通过 SFTP 方式挂载远程主机目录。

提示

  • httpfs2-enhanced 最好使用支持 SSL 和多线程的版本,可以加快响应速度并能挂载 HTTPS 的远程文件;
  • 我在测试中发现 httpfs2-enhanced 工具在国内的网络环境下挂载某些 HTTP 远程文件存在一些问题,因此做了简单的修改,并 fork 到我自己的 GitHub 仓库里了。有需要的朋友可以参考我修改过的 httpfs2-enhanced,检出其中的 http-fix 分支即可,我也已经针对原项目提交了 Pull request。

为了方便使用,我给原来的 uml-linux.sh 执行脚本增加了新的 uml.conf 配置文件:

UML_ID="umlvm"
UML_MEM="64M"

UML_NETMODE="slirp"
#HOST_ADDR="192.168.0.3"
#UML_ADDR="192.168.0.4"
#UML_DNS=""

TAP_DEV="tap0"
ROUTER_DEV="eth0"
VDE_SWITCH=""

REV_TCP_PORTS=""
REV_UDP_PORTS=""
#FWD_TCP_PORTS="111 892 2049 32803 662"
#FWD_UDP_PORTS="111 892 2049 947 32769 662 660"

简单说明如下:

  • UML_MEM 指定为 UML 系统分配多少内存,默认 64 MB;
  • UML_NETMODE 比较重要,指定 UML 系统的网卡模式,目前支持 tuntapslirpvde 这三个选项;
  • 如果是 tuntap 网卡模式,TAP_DEV 指定 host 主机上的 TUN 网卡设备名称,可以使用 HOST_ADDR 配置 host 主机 TUN 网卡的 IP 地址,UML_ADDR 配置 UML guest 主机的 IP 地址(最好和 HOST_ADDR 在同一网段),如果需要端口转发,那么还需要修改 ROUTER_DEV 指定 host 主机用于转发的物理网卡设备名称;
  • 如果是 slirp 网卡模式,那么会直接使用 Slirp 默认固定的专用地址:10.0.2.2 为 host 主机地址,10.0.2.15 为 UML guest 主机的 IP 地址,并自动将 UML guest 系统内的 DNS 服务器地址设置为 10.0.2.3 通过 host 主机进行域名解析;
  • 如果是 vde 网卡模式,那么必须修改 VDE_SWITCH 指定 VDE 软件交换机的路径,VDE 软件交换机需要通过 vde_switch 等命令预先配置,详细使用说明可以参考 Virtualsquare VDE Wiki 页面;
  • FWD_TCP_PORTSFWD_UDP_PORTS 指定进行转发的 TCP 和 UDP 端口列表(多个转发端口以空格隔开),转发端口支持 10080-80 这种形式(表示将 host 主机的 10080 端口转到 UML guest 的 80 端口),上面 uml.conf 中的注释列出来的是 UML 系统中对外的 NFS 服务器所需要的端口(根据实际情况也可以只允许 111、892、2049 这几个端口)。

为了能够根据 uml.conf 文件配置 Slirp 的端口转发功能,我还为 slirp 程序增加了一个 slirp.sh wrapper 脚本:

#!/bin/sh
DIR="$( cd "$( dirname "$0" )" && pwd )"

[ -f $DIR/uml.conf ] && . $DIR/uml.conf

CMD=`which slirp-fullbolt 2>/dev/null || which slirp`

for i in $FWD_TCP_PORTS; do
	if [ "x$i" = "x*" ]; then
		continue
	fi
	CMD="$CMD \"redir ${i%-*} ${i##*-}\""
done
for i in $FWD_UDP_PORTS; do
	if [ "x$i" = "x*" ]; then
		continue
	fi
	CMD="$CMD \"redir udp ${i%-*} ${i##*-}\""
done

eval "exec $CMD"

由于 UML kernel 调用 slirp 程序时不支持附加参数,这里才通过 slirp.sh 脚本来实现,功能也非常简单,通过 slirp 程序的 redir 选项配置端口转发。

提示

为了让 UML guest 系统的 Slirp 网络真正达到接近 host 主机的网络性能,host 主机上编译 slirp 程序时必须打开 FULL_BOLT 开关,并为 slirp 源代码打上 real full bolt 的 patch,否则 UML guest 系统内通过 Slirp 访问外网的速度会很慢。

值得庆幸的是 Debian、Ubuntu 系统自带的 slirp 软件包一般都打上了这个 patch,而且提供了 slirp-fullboltslirp 这两个程序分别对应 FULL_BOLT 开启和关闭的 Slirp,我的 slirp.sh 脚本也对此做了判断,优先使用速度更快的 slirp-fullbolt 程序。

至于新的启动 UML 系统的脚本 uml-linux.sh 稍微有点长,这里就不贴出来了,和原来相比的改动就是增加对 Slirp 和 VDE 网络的支持。

另外新的 uml-linux.sh 脚本改为默认前台方式启动 UML 系统;如果需要以后台方式启动 UML 系统,则可以用 uml-linux.sh -D 的方式来运行。

使用 UML 挂载 FUSE 文件系统

修改之后支持 FUSE 和 NFS 服务器的 UML 系统可以从这里下载(百度网盘备用):

https://zohead.com/downloads/uml-fuse-nfsd-x64.tar.xz
https://pan.baidu.com/s/1bp6l7B5

解压缩下载下来的 uml-fuse-nfsd-x64.tar.xz 文件,运行其中的 uml-linux 脚本就可以启动 UML 系统了,此 UML 系统的 root 用户默认密码为 uml

下面我以挂载 HTTP 远程 iso 文件中的安装包为例,介绍如何在 UML 系统中使用 FUSE。

首先使用 httpfs2 挂载阿里云上的 CentOS 6.9 iso 文件(这里为 httpfs2 指定 -c /dev/null 参数是为了去掉 httpfs2 的网络访问输出日志),挂载成功之后可以查看挂载点中的文件:

~ # httpfs2 -c /dev/null http://mirrors.aliyun.com/centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso /tmp
file name:      CentOS-6.9-x86_64-minimal.iso
host name:      mirrors.aliyun.com
port number:    80
protocol:       http
request path:   /centos/6/isos/x86_64/CentOS-6.9-x86_64-minimal.iso
auth data:      (null)
httpfs2: main: connecting to mirrors.aliyun.com port 80.
No SSL session data.
httpfs2: main: closing socket.
httpfs2: main: connecting to mirrors.aliyun.com port 80.
httpfs2: main: keeping socket open.
file size:      427819008
httpfs2: main: closing socket.
~ # ls -l /tmp
total 0
-r--r--r--    1 root     root     427819008 Mar 29  2017 CentOS-6.9-x86_64-minimal.iso

我们可以发现 httpfs2 的挂载点中实际上就只有一个远程文件名,我们可以直接挂载这个 iso 文件:

~ # mount -t iso9660 -o ro,loop /tmp/CentOS-6.9-x86_64-minimal.iso /media
~ # ls /media
CentOS_BuildTag                GPL                            RPM-GPG-KEY-CentOS-6           RPM-GPG-KEY-CentOS-Testing-6   isolinux
EFI                            Packages                       RPM-GPG-KEY-CentOS-Debug-6     TRANS.TBL                      repodata
EULA                           RELEASE-NOTES-en-US.html       RPM-GPG-KEY-CentOS-Security-6  images

到这里就可以直接查看远程 iso 文件中的软件包了,你可以很方便地将远程 iso 中的软件包拷贝到 host 主机的文件系统中。

如果你想直接拷贝软件包中的特定文件,也可以通过 archivemount 来实现哦:

~ # archivemount /media/Packages/sed-4.2.1-10.el6.x86_64.rpm /mnt
fuse: missing mountpoint parameter
~ # ls /mnt
bin  usr

这样直接复制软件包中的文件就实在太方便了(请忽略 archivemount 时的 fuse 警告,实际不影响使用)。

当然如果你想挂载 FTP 远程文件就可以通过 curlftpfs 命令来实现,也可以使用 mount.davfs 命令挂载 WebDAV 服务器上的文件(例如直接访问坚果云中的文件)。

导出 UML FUSE 文件系统

有些情况下我们需要将 UML 系统中的 FUSE 挂载点导出给 host 主机或者网络中的其它主机使用,这时可以通过 hostfs、SFTP、NFS 等方式实现,分别简单说明一下。

hostfs 导出 FUSE

这是最简单的方式,由于 UML 直接使用 hostfs 访问 host 主机的文件系统,因此 UML 系统内可以直接将 FUSE 中的文件拷贝到 hostfs 文件系统,只是这种方式 host 主机并不能直接访问 UML guest 主机的 FUSE 文件系统。

SFTP 导出 FUSE

这种方式也很简单,由于 UML 系统启动时自动运行了 dropbear 服务,我们可以先修改 uml.conf 配置文件设置 SSH 端口转发,将 host 主机的 2222 端口通过 Slirp 转发到 UML guest 系统内的 22 端口(如果 host 主机本身并没有运行 SSH 服务器,那甚至可以配置为 FWD_TCP_PORTS="22" 直接转发 22 端口):

FWD_TCP_PORTS="2222-22"

此时 host 主机就可以使用 scp 命令直接从 UML 的 FUSE 文件系统中拷贝文件了:

root@instance-007a20ff:~# scp -P 2222 root@192.168.1.52:/mnt/bin/sed /home

注意

上面命令中的 192.168.1.52 应根据实际情况替换为 host 主机上实际访问网络的网卡 IP 地址,不能使用 localhost 或者 127.0.0.1,因为 slirp 默认会自动选择访问网络的网卡,并不会进行本地转发。

NFS 导出 FUSE

首先需要修改 uml.conf 配置文件中的 FWD_TCP_PORTSFWD_UDP_PORTS 值,将默认的 NFS 服务需要的 TCP 和 UDP 端口注释去掉,表示将这些端口转发到 UML 系统内。

提示

如果你需要在 host 主机上直接 NFS 挂载 UML 系统里的 FUSE 文件系统,由于 sunrpc 的 111 端口无法修改而且需要转发到 UML 系统内,这种情况下 host 主机的 portmap 或者 rpcbind 服务需要保持关闭状态,防止 111 端口被占用。

使用 uml-linux.sh 脚本启动 UML 系统,启动完成之后通过集成的 FUSE 相关工具挂载需要访问的 FUSE 文件系统。

假设需要导出 UML 系统中的 /mnt FUSE 文件系统,UML 中默认的 NFS 导出目录配置文件 /etc/exports 如下:

/mnt 0.0.0.0/0.0.0.0(async,insecure,no_subtree_check,rw,no_root_squash,fsid=0)

上面的配置文件表示将 /mnt 目录通过 NFS 导出,默认允许所有主机访问,你可以根据需要修改导出目录路径和允许访问的主机地址。

接着在 UML 系统中通过集成的服务脚本启动 rpcbind 和 nfs 服务:

~ # /etc/init.d/rpcbind start
Starting up rpcbind...
~ # /etc/init.d/nfs start
Starting nfs service:
fs.nfs.nlm_tcpport = 32803
fs.nfs.nlm_udpport = 32769
Exporting directories for NFS...
Starting NFS daemon: 
rpc.nfsd: address family inet6 not supported by protocol TCP
NFSD: the nfsdcld client tracking upcall will be removed in 3.10. Please transition to using nfsdcltrack.
NFSD: starting 90-second grace period (net 000000006035a880)
Starting NFS mountd:

如果一切正常的话,此时就可以在外部通过 NFS 挂载 UML 系统导出的 FUSE 文件系统进行访问了:

root@instance-007a20ff:~# mount -t nfs -o nolock,proto=tcp,mountproto=tcp 192.168.1.52:/mnt /media

提示

nolock 参数是为了防止如果挂载的主机上没有运行 portmap 或者 rpcbind 服务导致挂载失败的问题(如果直接在 host 主机上挂载,host 的对应服务需要关闭);
proto=tcpmountproto=tcp 参数指定 NFS 数据和挂载请求都使用 TCP 协议,防止使用的随机 UDP 端口无法被 slirp 转发的问题。

这里需要说明的是如果直接在 Docker 容器(UML 的 host 主机)上挂载 NFS,虽然绝大多数 Docker 容器平台都默认支持 NFS 文件系统,但在非特权容器内部由于没有权限 mount 还是会失败的。

总结

本文介绍的在容器中使用 UML 挂载 FUSE 文件系统并通过 NFS 导出 UML 文件系统的方法是一个比较小众的需求,不过也算达到我的目的了。

对于非特权 Docker 容器来说,虽然还不能直接挂载 UML guest 的文件系统,但初步看起来还是可以通过 LKL(Linux Kernel Library)在应用层访问 UML 网络并实现 NFS 挂载的,只是 LKL 库的 NFS 挂载目录并不能直接给 Docker 容器中的普通应用程序使用。

最后祝大家在即将到来的 2018 年能继续玩地开心 ^_^。

  1. paul:

    您好,我阅读了您所著的这篇文章,对我帮助很大,解决了我的燃眉之急,非常感谢您将这种解决问题的思路提供给大家。

    但是,我在实践您文中的方法时遇到了一些问题,想寻求您的帮助:我在docker中启动UML后,试图挂载webDAV成功,但是我挂载的文件系统需要能被host的某个服务程序(直说吧,一个数据库)访问到,请问需要如何操作?是否需要终端复用才能同时开启多个应用?

    最后再次对您的无私奉献表示感谢

  2. paul:

    您好,我阅读了您所著的这篇文章,对我帮助很大,解决了我的燃眉之急,非常感谢您将这种解决问题的思路提供给大家。

    但是,我在实践您文中的方法时遇到了一些问题,想寻求您的帮助:

    1. 我在docker中启动UML后,试图挂载webDAV不成功:

    1.1 执行mount.davfs https://webdav.yandex.ru /mnt/webdav后返回mount.davfs: can’t change group of directory /var/run/mount.davfs: Operation not permitted
    1.2 执行mount -t davfs https://webdav.yandex.ru /mnt/webdav后返回mount: mounting https://webdav.yandex.ru on /mnt/webdav failed: No such device

    2. 挂载的webDAV文件系统需要能被host的某个服务程序(直说吧,一个数据库)访问到,请问需要如何操作?是否需要终端复用才能同时开启多个应用?

    最后再次对您的无私奉献表示感谢

  3. Uranus Zhou:

    挂载的问题需要确认 UML 是不是以 root 用户启动的;
    还有 ls -dl /var/run/mount.davfs 看看所有者和群组是不是都是 davfs2;

    要让 UML 中的文件系统能被 host 程序使用,就是参考导出 UML FUSE 文件系统 这一节了,看你的 Docker 中能否挂载 NFS,不能直接挂载的话是比较麻烦的。

  4. Puteulanus:

    你好,我用 Docker 的 Ubuntu:1604 镜像尝试运行你编译的 UML 系统,在 Checking that ptrace can change system call numbers 的时候,提示 ptrace: Operation not permitted ,添加 --cap-add=SYS_PTRACE 之后提示 /dev/shm must be not mounted noexec。

    我查到另一个在 Docker 里运行 UML 的镜像( https://hub.docker.com/r/weberlars/diuid/ ) 同样要求 SYS_PTRACE 权限,并使用 tmpfs 挂载了一个 umlshm。

    因为看到文章开头说环境是非特权容器,Bluemix 的 Docker 服务又已经关闭了,所以没有办法在相同的环境测试,不知道是 Bluemix 的容器本身带有所需的权限还是比如 Docker 的后续更新导致的。请问这两个是在 Docker 容器里运行 UML 必需的吗?或者是 UML 有选项关闭相应特性的。

    log(容器中):
    root@f345eb93aa08:~/uml# ./uml-linux.sh
    Core dump limits :
    soft - NONE
    hard - NONE
    Checking that ptrace can change system call numbers…ptrace: Operation not permitted
    check_ptrace : expected SIGSTOP, got status = 9

  5. Uranus Zhou:

    UML 是必须依赖 ptrace 的,我之前也在 OpenVZ 容器中测试过,
    你说的添加 --cap-add=SYS_PTRACE 之后提示 /dev/shm must be not mounted noexec,那是否可以 remount /dev/shm 把 noexec 选项给去掉?
    现在 Bluemix 关闭了,如果你能访问已经被墙的 Arukas 的话也可以用这个服务试试。
    另外 Win10 自带的 WSL 环境中运行 UML kernel 也会报类似的错误。

  6. Puteulanus:

    如果是必须依赖的话,大概就是 Bluemix 的容器本身在运行的时候添加了一些诸如 SYS_PTRACE 的权限了。。shm 的问题我看 https://github.com/moby/moby/issues/6758 大概是无法从容器内部解决的。

    weberlars/diuid 文档中的 --cap-add=SYS_PTRACE -e TMPDIR=/umlshm --tmpfs /umlshm:rw,nosuid,nodev,exec,size=8g 应该已经是在 Docker 中运行 UML 的最简单的参数了。

    本来是想能在不添加额外参数的 Docker 容器里运行的 _(:з」∠)_,加权限的话用 UML 的意义就比较小了。。

  7. eatcosmos:

    您好,在没用特权的远程docker里,我直接下载的tar.xz文件,然后解压直接运行 ./uml-linux.sh
    但是报错了,这是可能是什么原因的?有调试的办法吗?


    console [mc-1] enabled
    read_cow_header - short header
    VFS: Mounted root (hostfs filesystem) on device 0:11.
    devtmpfs: mounted
    This architecture does not have kernel memory protection.
    Kernel panic - not syncing: Attempted to kill init! exitcode=0x0000000b

    CPU: 0 PID: 1 Comm: init Not tainted 4.10.1 #17
    Stack:
    6482bc00 60190e70 60190e70 6482bc00
    6008c5a1 6008bccb 6440df00 3000000010
    6482bcd0 6482bbe0 6440dc00 0000000b
    Call Trace:
    [] ?
    printk+0x0/0x9b
    [] ?
    os_is_signal_stack+0x15/0x30

  8. Uranus Zhou:

    Docker 环境是网上的容器服务还是自己搭的哦?还有容器镜像是哪个?
    我抽空用本地 Docker 或 LXC 容器环境确认试试。

  9. eatcosmos:

    是在线的docker服务,用的这个网站的 https://git.openi.org.cn/zeizei/OpenI_Learning/debugjob?debugListType=all,里面的调试任务是执行在docker容器里,用frp把ssh映射出来的
    感觉好像对docker和主机环境依赖挺大的,但道理作为进程存在,就类似普通软件,任何环境都能运行





*