> For the complete documentation index, see [llms.txt](https://handbook.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://handbook.bsdcn.org/di-17-zhang-jail-yu-rong-qi/17.3.-zhu-ji-pei-zhi-jail.md).

# 17.3.主机配置

在宿主系统上创建任何 jail 之前，必须做一些配置，并获取宿主系统的一些信息。

这包括配置 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 工具、创建用于配置和安装 jail 的必要目录、获取宿主系统的网络信息，以及检查宿主是否使用 OpenZFS 或 UFS 作为文件系统。

> **技巧**
>
> jail 中运行的 FreeBSD 版本不得高于宿主系统运行的版本。

## 17.3.1. jail 工具

[jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 工具用于管理 jail。

若要在系统启动时自动启动 jail，执行以下命令：

```sh
# sysrc jail_enable="YES"
# sysrc jail_parallel_start="YES"
```

> **技巧**
>
> 启用 `jail_parallel_start` 后，所有已配置的 jail 将在后台同时启动。

默认情况下，配置文件中有条目的每个 jail 都会在系统启动时启动。如果只想启动选定的 jail，可在 `jail_list` 中列出它们的名称：

```sh
# sysrc jail_list="www db"
```

`jail_list` 中的名称以空格分隔，必须与 **/etc/jail.conf** 或 **/etc/jail.conf.d/** 中定义的 jail 名称匹配。只有列表中的 jail 会在启动时启动，并按给定顺序启动，因此未在 `jail_list` 中的 jail 必须手动启动。当 `jail_list` 为空时，配置文件中定义的所有 jail 都会启动。

要在关机时按 `jail_list` 的相反顺序停止 jail，可设置 `jail_reverse_stop`：

```sh
# sysrc jail_reverse_stop="YES"
```

> **注意**
>
> 启用 `jail_parallel_start` 后，jail 在后台启动，`jail_list` 的顺序不再得到保证。当某个 jail 必须在另一个 jail 启动之前运行时，请在 **/etc/jail.conf** 中使用 `depend` 参数来表达这种依赖关系，而不是依赖列表顺序。jail 的依赖会在其启动之前自动创建，并在其停止时以相反顺序停止，这与 `jail_list` 无关。`depend` 参数将在 [jail 配置文件](/di-17-zhang-jail-yu-rong-qi/17.3.-zhu-ji-pei-zhi-jail.md) 一节中介绍。

## 17.3.2. 网络

FreeBSD jail 的网络可通过几种不同的方式配置：

**宿主网络模式（IP 共享）** 在宿主网络模式中，jail 共享宿主的网络协议栈，而不是获得自己的协议栈。共享协议栈并不意味着共享地址：常见做法是为 jail 分配独立的 `ip4.addr`（和 `ip6.addr`），由 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 作为别名添加到宿主接口上，因此 jail 和宿主在同一协议栈上通过不同地址应答。只有配置为 `ip4 = inherit` 的 jail 才会完整共享宿主的地址，应答宿主自己的地址，并无限制地访问宿主持有的所有地址。

**虚拟网络（VNET）** 虚拟网络是 FreeBSD jail 提供的高级网络功能，比基础的宿主网络模式更灵活。VNET 能为每个 jail 创建独立的网络协议栈，为其分配独立的 IP 地址、路由表和网络接口。这提供了更高程度的网络隔离，使 jail 的运行行为更像是独立的虚拟机。

**netgraph 系统** [netgraph(4)](https://man.freebsd.org/cgi/man.cgi?query=netgraph\&sektion=4\&format=html) 是通用的内核框架，用于创建自定义网络配置。它可用于定义 jail 与宿主之间、以及多个 jail 之间网络流量的流向方式。

### 17.3.2.1. jail 的 IP 地址来源

共享协议栈的 jail 通过宿主接口上的地址访问网络。`interface` 参数指定该接口，对于 `ip4.addr` 和 `ip6.addr` 中的每个地址，[jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 都会自行管理别名：正如 jail(8) 所述，“在创建 jail 之前，会为每个地址在接口上添加一个别名，并在移除 jail 之后从接口上移除该别名”。宿主上无需预先配置该地址。

也可以按地址指定接口，从而用一行将地址与承载它的接口绑定：

```sh
ip4.addr = "em0|192.168.1.151/24";
```

当地址必须独立于 jail 持久存在时——因为宿主上的另一个服务依赖它，或者因为未使用 `interface` 参数——可在宿主上将其配置为 rc.conf 别名，不交给 jail(8) 管理：

```sh
# sysrc ifconfig_em0_aliases="inet 192.168.1.151/32"
```

较早的编号形式 `ifconfig_em0_alias0="inet 192.168.1.151/32"` 仍然可用，但对于新配置，推荐使用 `_aliases` 形式，因为它避免了编号别名必须形成连续 `alias0`、`alias1`、`alias2` 序列这种脆弱的要求。

### 17.3.2.2. 共享 IP jail 中的回环地址

共享协议栈的 jail 没有自己的回环接口。FreeBSD 内核对此做了补偿：在非 VNET jail 中，只要 `127.0.0.1` 或 `::1` 本身不是 jail 自己的地址之一，内核就会将回环地址上的 `bind()` 和 `connect()` 静默改写为 jail 的第一个 `ip4.addr` 或 `ip6.addr`。此行为未在 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 中记录，但它很重要，因为它带有安全隐患。

> **警告**
>
> 不要以为将服务绑定到 `localhost` 就能将其限制在 jail 内部。在共享 IP jail 中，被告知监听 `127.0.0.1` 的守护进程实际上绑定的是 jail 的第一个地址，这通常是面向局域网的地址，因此可从网络访问。

标准的缓解方法是给 jail 分配一个私有的回环地址并将其列在首位。在宿主上克隆一个专用的回环接口，并为其分配 `127.0.0.0/8` 范围内的地址（或私有 RFC 1918 地址）：

```sh
# sysrc cloned_interfaces="lo1"
# sysrc ifconfig_lo1="inet 127.0.1.1/32"
```

然后将该地址列在 jail 的 `ip4.addr` 中的首位，位于任何可路由地址之前，这样回环改写就会落到私有地址而非面向局域网的地址上。

### 17.3.2.3. 准备宿主服务

每个 jail 别名地址都共享宿主的端口。因此，绑定到通配符地址（在监听器列表中显示为 `*:port`）的宿主守护进程也会监听每个 jail 别名，并在 jail 自己的服务可以占用该端口之前就将其占用。在启动任何 jail 之前，先审计宿主已监听的内容：

```sh
# sockstat -4 -6 -l
```

限制每个绑定通配符地址的守护进程，使其仅使用宿主自己的地址，将 jail 地址留空：

* [syslogd(8)](https://man.freebsd.org/cgi/man.cgi?query=syslogd\&sektion=8\&format=html)：在 **/etc/rc.conf** 中设置 `syslogd_flags="-ss"`。单个 `-s` 已经使 syslogd 以安全模式运行，不监听远程日志消息；根据 syslogd(8)，如果指定两次 `-s`，“将完全不打开网络套接字”，这同时也会阻止 syslogd 向远程主机发送消息。
* [sshd(8)](https://man.freebsd.org/cgi/man.cgi?query=sshd\&sektion=8\&format=html)：在 **/etc/ssh/sshd\_config** 中将 `ListenAddress` 固定到宿主的地址，而不是保留默认的通配符。
* [ntpd(8)](https://man.freebsd.org/cgi/man.cgi?query=ntpd\&sektion=8\&format=html) 和 [sendmail(8)](https://man.freebsd.org/cgi/man.cgi?query=sendmail\&sektion=8\&format=html)：通过它们各自的配置将它们绑定到宿主地址。

关于哪些地址可由 jail 使用的防火墙级控制，请参见 [防火墙](/di-34-zhang-fang-huo-qiang/34.1.-gai-shu.md) 一章。

## 17.3.3. 设置 jail 目录结构

jail 的文件不需要放在特定路径。

有些管理员使用 **/jail**，有些使用 **/usr/jail**，也有些使用 **/usr/local/jails**。本章将使用 **/usr/local/jails**。

除了 **/usr/local/jails**，还将创建以下目录：

* **media**：用于存放下载的用户空间压缩包。
* **templates**：使用瘦 jail 时用于存放模板。
* **containers**：用于存放各个 jail。

如果使用 OpenZFS，执行以下命令为这些目录创建数据集：

```sh
# zfs create -o mountpoint=/usr/local/jails zroot/jails
# zfs create zroot/jails/media
# zfs create zroot/jails/templates
# zfs create zroot/jails/containers
```

> **技巧**
>
> 此处使用 `zroot` 作为父数据集，也可使用其他数据集。

为每个 jail 创建单独的子数据集，是实现每个 jail 独立快照、克隆、配额和数据集委托的基础。由于每个 jail 目录都是独立的数据集，OpenZFS 可以使用 `quota` 和 `reservation` 属性为其磁盘使用设置上限和保证；参见 [jail 资源限制](/di-17-zhang-jail-yu-rong-qi/17.13.-jail-zi-yuan-xian-zhi.md)。

如果使用 UFS，执行以下命令创建这些目录：

```sh
# mkdir /usr/local/jails/
# mkdir /usr/local/jails/media
# mkdir /usr/local/jails/templates
# mkdir /usr/local/jails/containers
```

## 17.3.4. jail 配置文件

配置 jail 有两种方式。

第一种是在 **/etc/jail.conf** 文件中为每个 jail 添加条目。另一种方式是在 **/etc/jail.conf.d/** 目录中为每个 jail 创建独立的配置文件。

如果宿主系统上只运行少量 jail，可直接将每个 jail 的配置写入 **/etc/jail.conf** 文件。若宿主系统上运行多个 jail，建议在 **/etc/jail.conf.d/** 目录下为每个 jail 分别创建配置文件。

**/etc/jail.conf.d/** 中的文件必须使用 `.conf` 作为扩展名，并且需要在 **/etc/jail.conf** 中通过以下语句引入：

```sh
.include "/etc/jail.conf.d/*.conf";
```

典型的 jail 配置条目如下：

```sh
jailname { ①
  # 启动与日志
  exec.start = "/bin/sh /etc/rc"; ②
  exec.stop = "/bin/sh /etc/rc.shutdown"; ③
  exec.consolelog = "/var/log/jail_console_${name}.log"; ④

  # 权限设置
  allow.raw_sockets; ⑤
  exec.clean; ⑥
  mount.devfs; ⑦

  # 主机名与路径
  host.hostname = "${name}"; ⑧
  path = "/usr/local/jails/containers/${name}"; ⑨

  # 网络配置
  ip4.addr = 192.168.1.151; ⑩
  ip6.addr = "2001:db8::151"; ⑪
  interface = em0; ⑫
}
```

* ① `jailname` jail 的名称。
* ② `exec.start` 在 jail 创建时运行的命令。常见写法为 `/bin/sh /etc/rc`。
* ③ `exec.stop` 在移除 jail 前运行的命令。常见写法为 `/bin/sh /etc/rc.shutdown`。
* ④ `exec.consolelog` 用于记录命令输出（stdout 和 stderr）的日志文件路径。
* ⑤ `allow.raw_sockets` 能在 jail 中创建原始套接字。启用后，可在 jail 中使用如 [ping(8)](https://man.freebsd.org/cgi/man.cgi?query=ping\&sektion=8\&format=html) 与 [traceroute(8)](https://man.freebsd.org/cgi/man.cgi?query=traceroute\&sektion=8\&format=html) 等工具。
* ⑥ `exec.clean` 在干净的环境中运行命令。
* ⑦ `mount.devfs` 将 [devfs(5)](https://man.freebsd.org/cgi/man.cgi?query=devfs\&sektion=5\&format=html) 文件系统挂载到 chroot 后的 **/dev** 目录，以限制 jail 内部可见的设备。它应用 `devfs_ruleset` 参数指定的规则集，或者当该参数保留为默认值 `0` 时，使用 `mount.devfs` 的回退规则集 4，即 `devfsrules_jail`，该规则集隐藏了除最小化 jail 安全设备集之外的所有宿主设备（参见 [devfs 规则集](/di-17-zhang-jail-yu-rong-qi/17.3.-zhu-ji-pei-zhi-jail.md)）。
* ⑧ `host.hostname` jail 的主机名。
* ⑨ `path` jail 的根目录路径。通过 `jail` 或 [jexec(8)](https://man.freebsd.org/cgi/man.cgi?query=jexec\&sektion=8\&format=html) 执行的命令均在该目录下运行。
* ⑩ `ip4.addr` 分配给 jail 的一个 IPv4 地址，或以逗号分隔的地址列表。设置 `ip4.addr` 隐含了 `ip4 = new`；`ip4` 本身不会保留为独立的默认值。`ip4` 参数接受三个值：`new` 将 jail 限制为 `ip4.addr` 中列出的地址，`inherit` 让 jail 无限制地访问宿主的所有 IPv4 地址，`disable` 完全关闭 jail 中的 IPv4。未设置 `ip4` 或 `ip4.addr` 的 jail 行为类似于 `inherit`，此时 `interface` 和 `ip4.addr` 在 `inherit` 下无效。在有多个地址的 jail 中，`ip4.saddrsel` 让内核为未绑定套接字的出站连接选择源地址，而 `ip4.nosaddrsel` 强制使用列表中的第一个地址。
* ⑪ `ip6.addr` 分配给 jail 的一个 IPv6 地址，或以逗号分隔的地址列表。`ip6` 参数接受与 `ip4` 相同的 `new`、`inherit` 和 `disable` 值，`ip6.saddrsel` 和 `ip6.nosaddrsel` 控制源地址选择。对于仅 IPv4 的 jail，使用 `ip6 = disable`；对于仅 IPv6 的 jail，使用 `ip4 = disable`。
* ⑫ `interface` 用于添加 jail IP 地址的网络接口，通常为宿主系统的接口名。

> **注意**
>
> jail 没有虚拟控制台：[init(8)](https://man.freebsd.org/cgi/man.cgi?query=init\&sektion=8\&format=html) 和 [getty(8)](https://man.freebsd.org/cgi/man.cgi?query=getty\&sektion=8\&format=html) 不会在其中运行，因此没有可交互连接的对象。`exec.consolelog` 文件仅捕获 `exec.*` 命令的标准输出和标准错误，即 **/etc/rc** 启动脚本和 **/etc/rc.shutdown** 脚本的输出。它不是交互式控制台；如需交互，可使用 [jexec(8)](https://man.freebsd.org/cgi/man.cgi?query=jexec\&sektion=8\&format=html)（参见 [访问 jail](/di-17-zhang-jail-yu-rong-qi/17.10.-guan-li-jail.md)）或在 jail 内运行 [sshd(8)](https://man.freebsd.org/cgi/man.cgi?query=sshd\&sektion=8\&format=html)。jail 自己的守护进程会将后续输出发送到 jail 的 syslog。

> **技巧**
>
> `exec.consolelog` 文件会无限增长。可通过 [newsyslog(8)](https://man.freebsd.org/cgi/man.cgi?query=newsyslog\&sektion=8\&format=html) 添加单个 glob 条目来轮转，例如在 **/etc/newsyslog.conf.d/jail** 中：
>
> ```sh
> /var/log/jail_console_*.log            640  7     100  *     J
> ```

### 17.3.4.1. 全局设置、通配符和变量

在任何 jail 块之外设置的参数是通配符默认值：[jail.conf(5)](https://man.freebsd.org/cgi/man.cgi?query=jail.conf\&sektion=5\&format=html) 会将它们应用于每个 jail，就像写在 `*` 通配符节中一样。一次性设置共享参数可使每个 jail 块保持简短。

以美元符号开头、可选地用大括号括起的名称，会被替换为该参数或变量的值。内置的 `${name}` 会展开为 jail 自己的名称，自定义变量的定义方式与参数相同，但以美元符号开头。

以下 **/etc/jail.conf** 一次性定义了公共头信息，并仅覆盖每个 jail 不同的部分：

```sh
# 应用于每个 jail 的共享默认值
exec.start = "/bin/sh /etc/rc";
exec.stop = "/bin/sh /etc/rc.shutdown";
exec.consolelog = "/var/log/jail_console_${name}.log";
exec.clean;
mount.devfs;
host.hostname = "${name}";
path = "/usr/local/jails/containers/${name}";
interface = em0;

www {
    ip4.addr = 192.168.1.151;
}

db {
    host.hostname = "database.example.com";
    ip4.addr = 192.168.1.152;
}
```

此处 `www` 继承了头信息中的所有内容，仅设置了自己的地址，而 `db` 还覆盖了 `host.hostname`。

### 17.3.4.2. 命令钩子

[jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 通过一系列生命周期钩子来运行 jail，每个钩子是在宿主环境还是在 jail 内部运行都很重要。创建顺序为：

`exec.prestart` 在创建 jail 之前于宿主环境运行。 `exec.created` 在 jail 已存在但尚未启动任何进程之前于宿主环境运行。 `exec.start` 在 jail 内部运行，通常为 `/bin/sh /etc/rc` 以启动其服务。 `exec.poststart` 在 jail 启动之后于宿主环境运行。

关闭时运行的是镜像过程：

`exec.prestop` 在停止 jail 之前于宿主环境运行。 `exec.stop` 在 jail 内部运行，通常为 `/bin/sh /etc/rc.shutdown`。 `exec.poststop` 在 jail 停止之后于宿主环境运行，用于拆除 `exec.prestart` 设置的内容。

可通过 `+=` 为一个钩子分配多个命令：

```sh
exec.poststart += "logger jail ${name} started";
```

`exec.timeout` 限制了 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 等待命令完成的时间，`exec.consolelog` 捕获这些命令的输出。[VNET jail](/di-17-zhang-jail-yu-rong-qi/17.6.-vnet-jail.md) 一节使用 `exec.prestart` 和 `exec.poststop` 在宿主上构建和拆除每个 jail 的网络管道。

钩子在单个 jail 内部为命令排序，而 `depend` 参数则为整个 jail 排序。写入 `depend = otherjail;` 可告知 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 该 jail 需要 `otherjail`：依赖项会在此 jail 创建之前首先创建，直到其最后一个 `exec.poststart` 命令完成，并且这些 jail 会以相反顺序拆除。

### 17.3.4.3. devfs 规则集

每个设置 `mount.devfs` 的 jail 都会获得一个 devfs 规则集，用于决定其 **/dev** 下出现哪些设备节点，该规则集要么是 `devfs_ruleset` 参数指定的，要么在该参数未设置时使用 `mount.devfs` 的默认规则集 4。[devfs.rules(5)](https://man.freebsd.org/cgi/man.cgi?query=devfs.rules\&sektion=5\&format=html) 在 **/etc/defaults/devfs.rules** 中定义命名规则集。其中两个面向 jail：`devfsrules_jail`（规则集 4）是标准的 jail 最小设备集，`devfsrules_jail_vnet`（规则集 5）额外暴露 **/dev/pf**，使 VNET jail 可以运行自己的防火墙。VNET jail 示例设置 `devfs_ruleset = 5`，Linux jail 示例设置 `devfs_ruleset = 4`；传统 jail 和瘦 jail 示例仅设置 `mount.devfs`，因此使用其默认的规则集 4。

要暴露额外设备，可在 **/etc/devfs.rules** 中编写自定义规则集，包含一个标准规则集并取消隐藏所需设备。以下规则集从 `devfsrules_jail` 开始，并额外取消隐藏 [bpf(4)](https://man.freebsd.org/cgi/man.cgi?query=bpf\&sektion=4\&format=html) 设备：

```sh
[devfsrules_jail_bpf=100]
add include $devfsrules_jail
add path 'bpf*' unhide
```

重新加载规则，然后在 jail 配置中按编号引用新规则集：

```sh
# service devfs restart
```

```sh
devfs_ruleset = 100;
```

**/etc/devfs.rules** 中的规则集会覆盖 **/etc/defaults/devfs.rules** 中同号的规则集；否则这两个文件将有效合并。

关于配置变量的更多信息，可参考 [jail(8)](https://man.freebsd.org/cgi/man.cgi?query=jail\&sektion=8\&format=html) 与 [jail.conf(5)](https://man.freebsd.org/cgi/man.cgi?query=jail.conf\&sektion=5\&format=html)。
