FreeBSD Linux Jail

FreeBSD 可以使用 Linuxulator: Linux执行程序兼容debootstrap 在jail中运行Linux。由于jail没有内核,所以物理主机上需要启用Linux二进制兼容:

备注

由于我的笔记本 FreeBSD无线网络BCM43602(通过wifibox) 需要运行Linux bhyve(BSD hypervisor) 虚拟机,所以已经启用了 Linuxulator: Linux执行程序兼容 支持

  • 在启动时启用Linux ABI:

启动时启用Linux ABI
sysrc linux_enable="YES"
  • 一旦配置了启动时启用Linux ABI,就可以立即用命令启用linux兼容,无需重启操作系统

启动Linux兼容
service linux start

准备 FreeBSD Thin(薄) Jail

为方便调整,我设置了环境变量来方便后续操作

设置 jail目录和release版本环境变量
export jail_dir="zdata/jails"
export bsd_ver="14.3"
# 在FreeBSD中root用户的shell默认是sh,所以调整 ~/.shrc
echo 'jail_dir="zdata/jails"' >> ~/.shrc
echo 'bsd_ver="14.3"' >> ~/.shrc

FreeBSD Thin(薄) Jail 初始化

需要先构建一个FreeBSD常规Jail,例如 FreeBSD Thin(薄) Jail ,但是不能直接执行配置,而是在配置前还有一个构建Linux兼容层的步骤。

(已完成,跳过)为模板创建发行版,这个是只读的:

在ZFS中创建模板数据集
zfs create -p zroot/jails/templates/14.2-RELEASE

(已完成,跳过)和 FreeBSD Thick(厚) Jail 一样下载用户空间:

下载用户空间
fetch https://download.freebsd.org/ftp/releases/amd64/amd64/$bsd_ver-RELEASE/base.txz -o /$jail_dir/media/$bsd_ver-RELEASE-base.txz

(已完成,跳过)将下载内容解压缩到模板目录:

内容解压缩到模板目录
tar -xf /usr/local/jails/media/14.2-RELEASE-base.txz -C /usr/local/jails/templates/14.2-RELEASE --unlink

(已完成,跳过)将时区和DNS配置复制到模板目录:

时区和DNS配置复制到模板目录
cp /etc/resolv.conf /usr/local/jails/templates/14.2-RELEASE/etc/resolv.conf
cp /etc/localtime /usr/local/jails/templates/14.2-RELEASE/etc/localtime

(已完成,跳过)更新模板补丁:

更新补丁
freebsd-update -b /usr/local/jails/templates/14.2-RELEASE/ fetch install

(已完成,跳过)从模板创建 ZFS 快照:

模板创建 ZFS 快照
zfs snapshot zroot/jails/templates/14.2-RELEASE@base

备注

以上步骤因为已经在实践 FreeBSD Thin(薄) Jail 时做过,只需要做一次,所以在这里创建 Linux jail 的时候仅记录,但跳过不支持。

下面的步骤才是实际创建名为 d2l 的thin jail

  • 需要执行 使用 OpenZFS 克隆功能创建名为 d2l 的thin jail,为后续构建 Linux jail 做准备:

使用 OpenZFS 克隆功能创建名为 d2l 的thin jail
zfs clone zroot/jails/templates/14.2-RELEASE@base zroot/jails/containers/d2l

第一次启动 FreeBSD Thin(薄) Jail d2l ( 我跳过这步 )

警告

我仔细看了FreeBSD Handbook,感觉手册中说第一次命令行启动 d2l 这样的jail,并在jail中安装 debootstrap ,然后执行 debootstrap 似乎有点折腾。

debootstrap 是可以直接把 Debian 系的操作系统直接复制到指定目录的,那么我为何不直接在物理主机上完成?

警告

我最终采用了跳过这步启动,而采用host主机上执行 debootstrap

备注

现在我们启动一个常规的 FreeBSD Thin(薄) Jail 名为 d2l ,但是还没有Linux兼容层的内容,需要启动以后通过 debootstrap 获取

首次命令 flying 方式启动 d2l FreeBSD Thin(薄) Jail
# 以下4个变量,这里假设SHELL是 sh 或 bash
NAME=d2l
HOSTNAME=d2l.cloud-atlas.io
IFACE=wifibox0
IP="10.0.0.9/24"

jail -cm \
  name="$NAME" \
  host.hostname="$HOSTNAME" \
  path="/usr/local/jails/containers/$NAME" \
  interface="$IFACE" \
  ip4.addr="$IP" \
  exec.start="/bin/sh /etc/rc" \
  exec.stop="/bin/sh /etc/rc.shutdown" \
  mount.devfs \
  devfs_ruleset=4 \
  allow.mount \
  allow.mount.devfs \
  allow.mount.fdescfs \
  allow.mount.procfs \
  allow.mount.linprocfs \
  allow.mount.linsysfs \
  allow.mount.tmpfs \
  enforce_statfs=1

这里采用命令方式而不是配置文件方式临时启动jail,以便能够进一步通过 debootstrap 来获得兼容

现在使用 jls 可以看到已经运行起来一个 d2l 的容器:

使用 jls 可以看到已经运行起 d2l jail
   JID  IP Address      Hostname                      Path
     1  10.0.0.9        d2l.cloud-atlas.io            /usr/local/jails/containers/d2l

进入jail通过 debootstrap 获取Linux( 我跳过这步 )

警告

我最终采用了跳过这步启动,而采用host主机上执行 debootstrap

  • 通过 jexec 进入 d2l jail:

通过 jexec 进入 d2l jail
jexec -u root d2l

# 进入jail以后执行以下2条命令
pkg install debootstrap
# 注意,我使用的是 debian stable 版本
# 如果要安装Ubuntu,则可以使用  jammy 或 noble
debootstrap stable /compat/debian
  • 完成后在host主机上停止 d2l :

停止 d2l jail
service jail onestop d2l

( 我的步骤 )直接Host执行 debootstrap 获取Linux

在host上完成 debootstrap
NAME=d2l
JAILPATH="/usr/local/jails/containers/$NAME"
LINUXPATH="$JAILPATH/compat/debian"

debootstrap stable $LINUXPATH

完成以后,在 /usr/local/jails/containers/d2l/compat/debian 目录下就是一个剥离出来独立的debian系统

  • 检查对比 /etc/jail.conf 配置,将 Linux Jail 差异部分都写入 /etc/jail.conf.d/d2l.conf 中:

Linux Jail差异部分配置在 /etc/jail.conf.d/d2l.conf
d2l {
  devfs_ruleset=4;
  ip4.addr="10.0.0.9/24";
  
  # MOUNT
  mount += "devfs     $path/compat/debian/dev     devfs     rw  0 0";
  mount += "tmpfs     $path/compat/debian/dev/shm tmpfs     rw,size=1g,mode=1777  0 0";
  mount += "fdescfs   $path/compat/debian/dev/fd  fdescfs   rw,linrdlnk 0 0";
  mount += "linprocfs $path/compat/debian/proc    linprocfs rw  0 0";
  mount += "linsysfs  $path/compat/debian/sys     linsysfs  rw  0 0";
  mount += "/tmp      $path/compat/debian/tmp     nullfs    rw  0 0";
  mount += "/home     $path/compat/debian/home    nullfs    rw  0 0";
}

备注

d2l.conf 配置中,最后两行挂载文件系统采用了 nullfs ,这是一种回环文件系统,用于在host主机和jail之间共享文件: 多个jail可以挂载相同的host主机目录

启动Linux Jail

启动 d2l Linux Jail
service jail start d2l

完成启动 d2l Linux Jail之后,在Host物理主机上可以看到容器挂载了Linux对应的设备文件系统以及procfs系统,所以此时在Host物理主机上执行 df -h 会看到:

在Host主机上 df -h 可以看到Linux的设备兼容层已经挂载
Filesystem                            Size    Used   Avail Capacity  Mounted on
...
zroot/jails/containers/d2l            903G    750M    902G     0%    /usr/local/jails/containers/d2l
devfs                                 1.0K      0B    1.0K     0%    /usr/local/jails/containers/d2l/compat/debian/dev
tmpfs                                 1.0G    4.0K    1.0G     0%    /usr/local/jails/containers/d2l/compat/debian/dev/shm
fdescfs                               1.0K      0B    1.0K     0%    /usr/local/jails/containers/d2l/compat/debian/dev/fd
linprocfs                             8.0K      0B    8.0K     0%    /usr/local/jails/containers/d2l/compat/debian/proc
linsysfs                              8.0K      0B    8.0K     0%    /usr/local/jails/containers/d2l/compat/debian/sys
/tmp                                  902G    7.8M    902G     0%    /usr/local/jails/containers/d2l/compat/debian/tmp
/home                                 902G     96K    902G     0%    /usr/local/jails/containers/d2l/compat/debian/home
devfs                                 1.0K      0B    1.0K     0%    /usr/local/jails/containers/d2l/dev

注意,对于Linux Jail的使用其实分两部分:

  • 直接使用 jexec d2l 进入的是常规 FreeBSD Jail

  • 执行以下 jexec 结合 chroot 将访问 Debian 系统Linux二进制兼容:

jexec 结合 chroot 将访问 Debian 系统Linux二进制兼容
jexec d2l chroot /compat/debian /bin/bash

此时虽然 df -h 看不出差别:

df -h 看到似乎和之前普通Jail一样挂载
Filesystem                  Size  Used Avail Use% Mounted on
zroot/jails/containers/d2l  903G  693M  903G   1% /

但是执行 ls /etc/ 就可以看到该目录下都是 debian 相关配置文件,例如 cat /etc/debian_version 可以看到版本是 12.8 。接下来的操作就好像是在 Debian 中进行,例如可以执行 apt update 更新debian系统

此时检查Linux环境:

使用 uname 检查Linux环境
uname -s -r -m

输出类似:

使用 uname 检查Linux环境输出案例
Linux 5.15.0 x86_64

異常排查

普通用户身份无法使用网络

我发现一个问题,我在 Linux Jail初始化 中为jail中创建了一个 admin 帐号(并设置了 sudo ):

Linux jail中创建用户组和用户admin
# 创建uid为1000的admin组
groupadd -g 1000 admin

# 创建admin用户 (/home/admin目录已经存在)
useradd -g 1000 -u 1000 -d /home/admin -s /bin/bash admin

# 安装sudo
apt install -y sudo curl

# 设置admin组用户(也就是admin)无需密码sudo
echo "%admin ALL=(ALL:ALL) NOPASSWD: ALL" >> /etc/sudoers.d/admin

# set TIMEZONE to Shanghai
unlink /etc/localtime
ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

但是这个 admin 用户默认无法使用网络,例如 ping www.baidu.com 提示错误:

Linux jail中普通用户无法使用网络错误
ping: socktype: SOCK_RAW
ping: socket: Protocol not supported
ping: => missing cap_net_raw+p capability or setuid?

只有通过 sudo ping www.baidu.com 才行。

这个问题仅在 chroot /compat/debian /bin/bash 之后才存在,如果没有进入Linux环境,仅仅是FreeBSD FreeBSD Thin(薄) Jail 环境普通用户是完全正常使用网络的。

也就是说Linux Jail中普通用户无法使用网络?

通过 getcap 命令可以获得程序能力设置属性( getcap: command not found ),不过,在linux jail环境中, getcap 执行没有效果(无返回内容)

Pinging from a base user wsl tumbleweed 5.15.90.1-microsoft-standard-WSL2 看起来Windows的WSL环境中普通用户也不能执行 ping

参考 ping as non-root fails due to missing capabilities #143 我尝试:

通过 setcap 为ping程序手工设置能力
sudo setcap cap_net_raw+ep /usr/bin/ping

但是设置失败:

通过 setcap 为ping程序手工设置能力,但是失败
unable to set CAP_SETFCAP effective capability: Operation not permitted
  • admin 用户使用 ssh 程序正常,可以远程访问其他系统

  • 似乎和UDP协议相关会失败,而TCP协议似乎正常:

    • 尝试 dig www.baidu.com 提示 communications error to 192.168.0.1#53: connection refused ,但是 nslookup www.baidu.com 能常常解析

    • 尝试 nc -v 192.168.0.1 53 显示TCP的53端口正常打开 Connection to 192.168.0.1 53 port [tcp/domain] succeeded! ,但是指定UDP协议 nc -uv 192.168.0.1 53 则直接返回无输出

备注

这个问题还在排查,目前仅发现是普通用户 admin 身份使用网络异常,其他执行脚本则正常(例如 安装Conda 运行下载后的脚本安装正常)

No internet access from inside jail! 提到设置 allow.raw_sockets; :

在Linux Jail /etc/jail.conf.d/d2l.conf 中添加允许 raw_sockets
d2l {
  allow.raw_sockets;
  devfs_ruleset=4;
  ip4.addr="10.0.0.9/24";
  
  # MOUNT
  mount += "devfs     $path/compat/debian/dev     devfs     rw  0 0";
  mount += "tmpfs     $path/compat/debian/dev/shm tmpfs     rw,size=1g,mode=1777  0 0";
  mount += "fdescfs   $path/compat/debian/dev/fd  fdescfs   rw,linrdlnk 0 0";
  mount += "linprocfs $path/compat/debian/proc    linprocfs rw  0 0";
  mount += "linsysfs  $path/compat/debian/sys     linsysfs  rw  0 0";
  mount += "/tmp      $path/compat/debian/tmp     nullfs    rw  0 0";
  mount += "/home     $path/compat/debian/home    nullfs    rw  0 0";
}

但是启动 d2l 容器之后,Linux的普通用户依然无法使用 pingdig 这样的UDP程序。

在没有 chroot /compat/debian /bin/bash 之前的FreeBSD Jail中可以检查Jail是否允许 raw_sockets

在FreeBSD Jail中(还没有chroot) sysctl 检查 allow_raw_sockets
# 在Jail中检查allow_raw_sockets
# 注意,是FreeBSD Jail,还没有chroot进入Linux层之前检查
sysctl security.jail.allow_raw_sockets

输出显示:

在FreeBSD Jail中(还没有chroot) sysctl 检查 allow_raw_sockets ,输出显示1表示允许
security.jail.allow_raw_sockets: 1

注意,默认情况下,在Host主机执行 sysctl security.jail.allow_raw_sockets 可以看到 security.jail.allow_raw_sockets: 0 ,也就是默认不允许 raw_sockets 。以上在FreeBS Jail中输出显示 1 是因为我在 d2l.conf 配置中添加了 allow_raw_sockets;

确实发现,在Linux Jail中,无法正常使用 ifconfigip 命令(在 How to have network access inside a Linux jail? 讨论中提到了在Linux Jail中使用 ip 命令总是错误 Cannot open netlink socket: Address family not supported by protocol )

Linux Jail无法使用 ifconfig
root@d2l:/# ifconfig
eth0: flags=4419<UP,BROADCAST,RUNNING,PROMISC,MULTICAST>  mtu 1500
        ether 58:9c:fc:10:ff:b4  (Ethernet)
        RX packets 6035264  bytes 8399983590 (7.8 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 3804835  bytes 322805861 (307.8 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

lo: flags=4169<UP,LOOPBACK,RUNNING,MULTICAST>  mtu 16384
        unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  (UNSPEC)
        RX packets 10473696  bytes 4994159902 (4.6 GiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 10473696  bytes 4994159902 (4.6 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

wifibo: error fetching interface information: Invalid argument

root@d2l:/# ifconfig eth0 10.1.1.10 netmask 255.255.255.0
SIOCSIFADDR: Invalid argument
SIOCSIFFLAGS: Invalid argument
SIOCSIFNETMASK: Invalid argument

root@d2l:/# ip addr
1: lo: <LOOPBACK,MULTICAST,UP> mtu 16384 qdisc noqueue state UP qlen 1000
    link/ieee1394 
2: wifibox0: <BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/[209] 58:9c:fc:10:60:55 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.9/24 brd 10.0.0.255 scope global dynamic wifibox0
3: eth0: <BROADCAST,MULTICAST,PROMISC,UP> mtu 1500 qdisc noqueue state UP qlen 1000
    link/ether 58:9c:fc:10:ff:b4 brd ff:ff:ff:ff:ff:ff

root@d2l:/# ip addr add 10.1.1.10/24 dev eth0
RTNETLINK answers: Operation not permitted

同样,由于没有socks支持,运行 Jupyter - 数据科学开发平台 :

在Linux Jail中运行 Jupyter - 数据科学开发平台 出现socket报错
[I 2025-01-07 14:08:32.945 ServerApp] notebook | extension was successfully loaded.
Traceback (most recent call last):
  File "/home/admin/conda/envs/d2l-zh/bin/jupyter-notebook", line 8, in <module>
    sys.exit(main())
             ^^^^^^
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/jupyter_server/extension/application.py", line 616, in launch_instance
    serverapp = cls.initialize_server(argv=args)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/jupyter_server/extension/application.py", line 586, in initialize_server
    serverapp.initialize(
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/traitlets/config/application.py", line 118, in inner
    return method(app, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/jupyter_server/serverapp.py", line 2822, in initialize
    self.init_httpserver()
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/jupyter_server/serverapp.py", line 2620, in init_httpserver
    self._find_http_port()
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/jupyter_server/serverapp.py", line 2667, in _find_http_port
    sockets = bind_sockets(port, self.ip)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/site-packages/tornado/netutil.py", line 128, in bind_sockets
    sock = socket.socket(af, socktype, proto)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/admin/conda/envs/d2l-zh/lib/python3.11/socket.py", line 232, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
OSError: [Errno 93] Protocol not supported

简单小结

简单规律就是在Linux Jail中 :

  • TCP协议栈是较为完整的

  • 不支持UDP协议栈: DNS查询需要DNS服务器支持TCP查询协议(UDP查询会失败 connection refused )

  • root 用户可以使用ICMP(ping),但是普通用户不能

  • Linux Jail没有socket支持,所以部分需要绑定sockets的服务,如 juypter 无法启动

  • 可能的解决方案: 采用 FreeBSD VNET Jail 来实现独立完整的网络堆栈

/home/admin 属主是root(错误)

admin 用户通过ssh登录到Jail中(尚未 chroot 进入debian linux),在尝试向 /compat/debian/home/admin 目录内复制文件,发现没有权限。检查发现 /compat/debian/home/admin 目录的属主是 root ,所以手工修订:

修订 /compat/debian/home/admin 目录属主
sudo chown admin:admin /compat/debian/home/admin

这里可能有一个问题,Linux系统 debootstrap 没有设置 /compat/debian/home/admin 内任何内容,这是因为当时目录存在,所以 useradd 命令没有使用 -m 参数,也就没有复制任何初始profile相关文件。这在后续 安装Conda 时就没有为用户设置好环境。

所以可能需要在一开始就修复好目录,或者直接删除掉目录,创建 admin 帐号时通过 useradd 构建

改进的思路

我期望在多个 Linux Jail 之间使用共享的 debootstrap 数据,思路是使用 在多个Jail间共享文件目录

参考