> For the complete documentation index, see [llms.txt](https://book.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.bsdcn.org/di-29-zhang-fang-huo-qiang/di-29.2-jie-ipfirewall-ipfw.md).

# 29.2 ipfirewall（IPFW）

> **警告**
>
> 本节内容未经实际测试，仅理论验证通过。请勿直接用于生产环境！建议在测试环境中验证后再用于生产部署。

IPFirewall（IPFW，IP Firewall）是为 FreeBSD 编写的状态防火墙，也是 FreeBSD 中最早的包过滤软件。IPFW 支持 IPv4 和 IPv6。它由多个组件组成：内核防火墙过滤规则处理器及其集成的数据包计数设施、日志设施、NAT、dummynet(4) 流量整形器（通过 dnctl 配置）、转发设施、桥接设施和 ipstealth 设施。

IPFW 最初由 Daniel Boulet 为 BSDI（Berkeley Software Design, Inc.）编写 API，后由 Ugen J. S. Antsilevich 大幅修改并移植到 FreeBSD，当前的 ipfw2 版本由 Luigi Rizzo 于 2002 年夏季为 FreeBSD 重写。

IPFW 未包含在 GENERIC 内核中，但以可加载内核模块（ipfw\.ko）的形式提供，也可通过内核配置选项 `options IPFIREWALL` 编译进自定义内核。

FreeBSD 在 **/etc/rc.firewall** 中提供了一个样例规则集，定义了几个常见场景的防火墙类型，帮助新手用户生成适合的规则集。IPFW 提供了强大的语法，进阶用户可用来定制符合特定环境安全要求的规则集。

作为 FreeBSD 原生防火墙，IPFW 采用基于规则编号的优先级机制。规则编号越小，优先级越高，可覆盖编号较大的规则。

> **警告**
>
> IPFW 默认包含一条规则，规则号为 `65535`，不可删除：该规则会阻断所有未匹配的流量。可通过内核编译选项 `IPFIREWALL_DEFAULT_TO_ACCEPT` 或在 **/boot/loader.conf** 中设置可调参数 `net.inet.ip.fw.default_to_accept` 来改为允许此默认行为，重启后生效，无法在运行时通过 `sysctl` 修改。
>
> 将默认策略改为 **允许** 在生产环境中极为危险：任何未显式匹配的流量都会被放行，等同于防火墙形同虚设。**此配置仅适用于测试环境，切勿在生产环境中使用。**
>
> 同时，在防火墙配置完成之前，切勿启动 IPFW，以免被阻断在防火墙之外。

本节说明了如何启用 IPFW，提供了其规则语法的概述，并展示了几种常见配置场景的规则集。

## 启用 IPFW

配置 IPFW 服务需要先启用系统防火墙，再执行启动与状态检查等操作，具体步骤如下。

可使用以下命令设置 IPFW 防火墙在系统启动时自动启动：

```sh
# service ipfw enable # 设置 rc 变量并尝试立即启动
```

> **技巧**
>
> 参见 FreeBSD 源代码文件 **libexec/rc/rc.d/routing**。

要使用 FreeBSD 提供的默认防火墙类型，需要再添加一行，指定类型：

```sh
# sysrc firewall_type="open"
```

可用的类型有：

| 类型            | 说明                 |
| ------------- | ------------------ |
| `open`        | 通过所有流量。            |
| `client`      | 仅保护此机器。            |
| `simple`      | 保护整个网络。            |
| `closed`      | 除回环接口外，完全禁用 IP 流量。 |
| `workstation` | 仅使用有状态规则保护此机器。     |
| `UNKNOWN`     | 禁用加载防火墙规则。         |
| **filename**  | 包含防火墙规则集的文件的完整路径。  |

若 `firewall_type` 设置为 `client` 或 `simple`，需要修改 **/etc/rc.firewall** 中的默认规则，以适应系统的配置。

注意，`filename` 类型用于加载自定义规则集。

另一种加载自定义规则集的方法是将 `firewall_script` 变量设置为包含 IPFW 命令的 *可执行脚本* 的绝对路径。本节中使用的示例假设 `firewall_script` 设置为 **/etc/ipfw\.rules**：

```sh
# sysrc firewall_script="/etc/ipfw.rules"
```

要通过 syslogd(8) 启用日志记录，需要添加以下行：

```sh
# sysrc firewall_logging="YES"
```

> **警告**
>
> 只有包含 `log` 选项的防火墙规则才会记录日志。默认规则不包括此选项，必须手动添加。因此，建议编辑默认规则集以启用日志记录。另外，若日志存储在单独的文件中，可能需要启用日志轮替。没有 **/etc/rc.conf** 变量可用于设置日志限制。要限制每次连接尝试记录规则的次数，需要在 **/etc/sysctl.conf** 中指定数字：

```sh
# echo "net.inet.ip.fw.verbose_limit=5" >> /etc/sysctl.conf
```

要通过专用接口 `ipfw0` 启用日志记录，需要改为在 **/etc/rc.conf** 中添加以下行：

```sh
# sysrc firewall_logif="YES"
```

然后使用 tcpdump 查看日志内容：

```sh
# tcpdump -t -n -i ipfw0
```

> **技巧**
>
> 除非附加了 tcpdump，否则不会产生日志记录的开销。

保存必要的编辑后，启动防火墙。要立即启用日志限制，还需要设置上述 `sysctl` 值：

```sh
# service ipfw start
# sysctl net.inet.ip.fw.verbose_limit=5
```

* 查看 IPFW 防火墙的当前状态：

```sh
# service ipfw status
```

## IPFW 规则语法

当数据包进入 IPFW 防火墙时，它会与规则集中的第一条规则进行比较，并按顺序逐条处理。当数据包与某条规则的选择条件匹配时，将执行该规则的动作，规则集的搜索随即终止。此过程称为“第一个匹配的规则生效”。若数据包未与任何规则匹配，则会被 IPFW 的强制性默认规则 65535 捕获，该规则会拒绝所有数据包并静默丢弃它们。然而，若数据包匹配了包含 `count`、`skipto` 或 `tee` 关键字的规则，搜索会继续进行。

```sh
数据包进入 IPFW
   │
   │  与第一条规则比较
   ▼
规则 N
   │
   ├── 匹配？── 否 ──► 下一条规则（N+1）
   │
   │ 是
   ▼
执行规则动作
   │
   ├── allow/pass：放行
   ├── deny/drop：丢弃
   ├── count：计数，继续匹配
   ├── skipto：跳转到指定规则
   └── tee：复制，继续匹配
   │
   │  若未匹配任何规则
   ▼
默认规则 65535（deny all）
```

创建 IPFW 规则时，关键字必须按以下顺序编写。某些关键字是必需的，其余为可选。大写字母表示变量，而小写字母必须位于紧随其后的变量之前。`#` 符号用于标记注释的开始，可出现在规则的末尾或单独占一行。空白行会被忽略。

`CMD RULE_NUMBER set SET_NUMBER ACTION log LOG_AMOUNT PROTO from SRC SRC_PORT to DST DST_PORT OPTIONS`

本节提供了这些关键字及其选项的概述，并非每个可能选项的详尽列表。

* **CMD** 每条规则必须以 `ipfw add` 开头。
* **RULE\_NUMBER** 每条规则都与一个从 `1` 到 `65535` 的编号相关联，编号 `65535` 保留为默认规则。该编号用于表示规则处理的顺序。多个规则可具有相同的编号，这种情况下它们会按添加顺序依次应用。
* **SET\_NUMBER** 每条规则都与一个从 `0` 到 `31` 的编号相关联。可单独禁用或启用，因此可快速添加或删除一组规则。若未指定 `SET_NUMBER`，则规则将被添加到 `0`。
* **ACTION** 每条规则可与以下一种动作关联。当数据包与规则的选择条件匹配时，将执行指定的动作。

  | 动作                                  | 说明                                                                                                                        |
  | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
  | `allow \| accept \| pass \| permit` | 这些关键字是等价的，允许匹配规则的数据包。                                                                                                     |
  | `check-state`                       | 检查数据包与动态状态表的匹配情况。若找到匹配项，则执行与生成此动态规则的规则关联的动作，否则移动到下一条规则。若规则集中没有 `check-state` 规则，则会在第一个 `keep-state` 或 `limit` 规则处检查动态规则表。 |
  | `count`                             | 更新匹配规则的所有数据包计数器。搜索会继续进行到下一条规则。                                                                                            |
  | `deny \| drop`                      | 悄悄丢弃匹配规则的数据包。                                                                                                             |
* **LOG\_AMOUNT** 当数据包匹配包含 `log` 关键字的规则时，将通过 syslogd(8) 记录一条消息，设施名称为 `SECURITY`。日志记录仅在特定规则记录的数据包数量未超过指定的 `LOG_AMOUNT` 时发生。若未指定 `LOG_AMOUNT`，则限制值将取自 `net.inet.ip.fw.verbose_limit` 的值。设置为零将移除日志记录限制。达到限制后，可通过 `ipfw resetlog` 清除该规则的日志计数器来重新启用日志；若要同时清除数据包计数器，请使用 `ipfw zero`。

> **注意**
>
> 日志记录在所有其他数据包匹配条件满足之后、对数据包执行最终动作之前发生。管理员决定在哪些规则上启用日志记录。

* **PROTO** 可选值，用于指定 **/etc/protocols** 中的协议名称或编号。
* **SRC** `from` 关键字必须跟随源地址或表示源地址的关键字。地址可由 `any`、`me`（此系统上任何接口配置的地址）、`me6`（此系统上任何接口配置的 IPv6 地址）或 `table` 后跟查找表的编号表示，查找表包含一个地址列表。当指定 IP 地址时，可选择后跟其 CIDR 掩码或子网掩码。例如，**1.2.3.4/25** 或 **1.2.3.4/255.255.255.128**。
* **SRC\_PORT** 可通过端口号或 **/etc/services** 中的端口名称指定可选的源端口。
* **DST** `to` 关键字必须跟随目标地址或表示目标地址的关键字。可使用与 `SRC` 部分相同的关键字和地址说明目标。
* **DST\_PORT** 可通过端口号或 **/etc/services** 中的端口名称指定可选的目标端口。
* **OPTIONS** 多个关键字可跟在源和目标之后。如其名所示，OPTIONS 是可选的。常用选项包括 `in` 或 `out`，用于指定数据包流动的方向，`icmptypes` 后跟 ICMP 消息类型，以及 `keep-state`。

  当匹配到 `keep-state` 规则时，防火墙将创建一个动态规则，匹配源和目标地址及端口之间的双向流量，使用相同的协议。

  动态规则设施容易受到资源耗尽攻击的影响，如 SYN 洪泛攻击，可能会打开大量动态规则。为应对此类攻击，可使用 `limit`。此选项通过检查已打开的动态规则并计算此规则和 IP 地址组合的出现次数，限制同时会话的数量。若此计数大于 `limit` 指定的值，则丢弃数据包。

  有许多 OPTIONS 可用。有关每个可用选项的说明，参阅 ipfw(8)。

## 示例规则集

本节演示如何创建状态防火墙规则集脚本 **/etc/ipfw\.rules**。在此示例中，所有连接规则均使用 `in` 或 `out` 明确方向，并使用 `via` *interface-name* 指定数据包经过的接口。

> **注意**
>
> 在首次创建或测试防火墙规则集时，考虑暂时设置这个可调参数：
>
> ```sh
> net.inet.ip.fw.default_to_accept="1"
> ```
>
> 此参数为只读可调参数，需要在 **/boot/loader.conf** 中设置，重启后生效，不可通过 `sysctl` 在运行时修改。这将把 ipfw(8) 的默认策略设置为比默认的 `deny ip from any to any` 更宽松，这样可在重启后稍微降低被锁定的风险。

防火墙脚本首先声明其为 sh 脚本，并清空所有现有规则。然后创建 `cmd` 变量，以便在每条规则开始时无需重复输入 `ipfw add`。还定义了 `pif` 变量，表示连接到互联网的接口名称。

```sh
#!/bin/sh
# 清空现有规则
ipfw -q -f flush

# 设置规则命令前缀变量
cmd="ipfw -q add"
pif="em0"     # 连接到互联网的外部网卡接口名称
```

前两条规则允许通过受信任的内部接口和回环接口的所有流量：

```sh
# 将 em1 改为实际的局域网网卡接口名称
$cmd 00005 allow all from any to any via em1

# 允许回环接口上的所有流量
$cmd 00010 allow all from any to any via lo0
```

接下来的规则在数据包与动态规则表中的现有条目匹配时允许其通过：

```sh
$cmd 00101 check-state
```

接下来的规则集定义了内部系统可创建到互联网上主机的哪些有状态连接：

```ini
# 允许访问公共 DNS 服务
# 将 x.x.x.x 替换为公共 DNS 服务器的 IP 地址，
# 并为 /etc/resolv.conf 中列出的每个 DNS 服务器重复此规则
$cmd 00110 allow tcp from any to x.x.x.x 53 out via $pif setup keep-state
$cmd 00111 allow udp from any to x.x.x.x 53 out via $pif keep-state

# 允许访问 ISP 的 DHCP 服务器，适用于电缆调制解调器或 ADSL 等宽带连接
# 默认使用记录日志的通用规则；确认 DHCP 服务器地址后，可将 x.x.x.x 填入下一条规则并启用它
$cmd 00120 allow log udp from any to any 67 out via $pif keep-state
#$cmd 00120 allow udp from any to x.x.x.x 67 out via $pif keep-state

# 允许外发 HTTP 和 HTTPS 连接
$cmd 00200 allow tcp from any to any 80 out via $pif setup keep-state
$cmd 00220 allow tcp from any to any 443 out via $pif setup keep-state

# 允许外发 SMTP 邮件提交连接
$cmd 00230 allow tcp from any to any 587 out via $pif setup keep-state

# 允许外发 ICMP（如 ping）
$cmd 00250 allow icmp from any to any out via $pif keep-state

# 允许外发 NTP 时间同步
$cmd 00260 allow udp from any to any 123 out via $pif keep-state

# 允许外发 SSH 连接
$cmd 00280 allow tcp from any to any 22 out via $pif setup keep-state

# 拒绝并记录所有其他外发连接
$cmd 00299 deny log all from any to any out via $pif
```

接下来的规则集控制从互联网主机到内部网络的连接。它首先拒绝通常与攻击相关的数据包，然后显式允许特定类型的连接。所有从互联网发起的授权服务均使用 `limit` 防止洪泛攻击。

```ini
# 拒绝所有来自不可路由保留地址空间的入站流量
$cmd 00300 deny all from 192.168.0.0/16 to any in via $pif     # RFC 1918 私有地址
$cmd 00301 deny all from 172.16.0.0/12 to any in via $pif      # RFC 1918 私有地址
$cmd 00302 deny all from 10.0.0.0/8 to any in via $pif         # RFC 1918 私有地址
$cmd 00303 deny all from 127.0.0.0/8 to any in via $pif        # 回环地址
$cmd 00304 deny all from 0.0.0.0/8 to any in via $pif          # 本网络地址
$cmd 00305 deny all from 169.254.0.0/16 to any in via $pif     # 链路本地地址（APIPA）
$cmd 00306 deny all from 192.0.2.0/24 to any in via $pif       # 文档示例保留地址
$cmd 00307 deny all from 224.0.0.0/3 to any in via $pif        # D 类组播与 E 类保留

# 拒绝来自外部的 ICMP 回显请求（禁止外部 ping 本机）
$cmd 00310 deny icmp from any to any in via $pif

# 拒绝 NetBIOS/SMB 相关端口的入站访问
$cmd 00320 deny tcp from any to any 137 in via $pif
$cmd 00321 deny tcp from any to any 138 in via $pif
$cmd 00322 deny tcp from any to any 139 in via $pif
$cmd 00323 deny tcp from any to any 445 in via $pif
$cmd 00324 deny udp from any to any 137 in via $pif
$cmd 00325 deny udp from any to any 138 in via $pif

# 拒绝分片数据包
$cmd 00330 deny all from any to any frag in via $pif

# 拒绝未匹配动态规则表的 ACK 包
$cmd 00332 deny tcp from any to any established in via $pif

# 允许来自 ISP DHCP 服务器的入站流量
# 将 x.x.x.x 替换为规则 00120 中使用的 DHCP 服务器 IP 地址
#$cmd 00360 allow udp from any to x.x.x.x 67 in via $pif keep-state

# 允许到内部 Web 服务器的 HTTP 连接，并限制同一源地址的同时连接数
$cmd 00400 allow tcp from any to me 80 in via $pif setup limit src-addr 2

# 允许入站 SSH 连接，并限制同一源地址的同时连接数
$cmd 00410 allow tcp from any to me 22 in via $pif setup limit src-addr 2

# 拒绝并记录所有其他入站连接
$cmd 00499 deny log all from any to any in via $pif
```

最后一条规则记录并拒绝所有不匹配上述规则的包：

```ini
# 拒绝并记录所有其他流量
$cmd 00999 deny log all from any to any
```

## 内核 NAT

FreeBSD 的 IPFW 防火墙提供内核 NAT 实现，与 IPFW 配合工作，提供网络地址转换。此功能可用于提供互联网连接共享方案，使多台内部计算机能够使用单个公共 IP 地址连接互联网。

为此，连接到互联网的 FreeBSD 机器必须充当网关。该系统必须有两个网卡，其中一个连接到互联网，另一个连接到内部局域网（LAN）。每台连接到 LAN 的计算机应分配一个私有网络空间的 IP 地址，如 [RFC 1918](https://www.ietf.org/rfc/rfc1918.txt) 所定义。

启用 IPFW 的内核 NAT 功能需要额外配置。要在引导时启用内核 NAT 支持，必须在 **/etc/rc.conf** 中设置以下内容：

```sh
gateway_enable="YES"
firewall_enable="YES"
firewall_nat_enable="YES"
```

> **注意**
>
> 设置了 `firewall_nat_enable` 而未设置 `firewall_enable` 时，将不产生任何效果，也不会执行任何操作。这是因为内核 NAT 实现仅与 IPFW 兼容。

当规则集包含有状态规则时，NAT 规则的位置非常关键，且需要使用 `skipto` 动作。`skipto` 动作需要指定规则号以确定跳转目标。下面的示例在前一节中展示的防火墙规则集的基础上构建，添加了一些额外的条目并修改了一些现有的规则，以配置内核 NAT。首先添加一些额外变量，表示跳转到的规则号、`keep-state` 选项以及用于减少规则数量的 TCP 端口列表。

```sh
#!/bin/sh
# 清空现有规则
ipfw -q -f flush
# 规则命令前缀
cmd="ipfw -q add"
# 出站有状态规则匹配后跳转到规则 1000 执行 NAT
skip="skipto 1000"
# 连接到互联网的外部网卡接口
pif=em0
# 启用有状态跟踪
ks="keep-state"
# 允许出站的 TCP 端口列表
good_tcpo="22,53,80,443,587"
```

还将配置一个 NAT 实例。可有多个 NAT 实例，每个实例都有自己的配置。此示例中只需一个 NAT 实例，即 NAT 实例 1。配置可包括几个选项，如：`if` 指定公共接口，`same_ports` 确保别名端口和本地端口号保持一致，`unreg_only` 仅处理未注册（私有）地址空间，`reset` 可在 IPFW 机器的公共 IP 地址发生变化时保持 NAT 实例正常工作。有关可传递给单个 NAT 实例配置的所有可能选项，参阅 ipfw(8)。配置有状态 NAT 防火墙时，需要允许转换后的数据包重新注入防火墙做进一步处理。在防火墙脚本开头禁用 `one_pass` 行为即可实现此目的。

```sh
# 允许经过 NAT 处理的数据包重新注入防火墙继续匹配后续规则
ipfw disable one_pass
# 配置 NAT 实例 1：使用指定外部接口，保持端口号一致，仅处理私有地址，IP 变化时重置
ipfw -q nat 1 config if $pif same_ports unreg_only reset
```

入站 NAT 规则应插入允许所有受信任和回环接口流量的两条规则之后，在重新组装规则之后、`check-state` 规则之前。重要的是，这条 NAT 规则的规则号（在此示例中为 `100`）必须大于前面三条规则的规则号，并且小于 `check-state` 规则的规则号。此外，由于内核 NAT 的行为，建议在第一个 NAT 规则之前并在允许受信任接口流量的规则之后放置一个重新组装规则。通常情况下，不应发生 IP 分片，但在处理 IPSEC/ESP/GRE 隧道流量时，可能会发生分片，必须先重新组装分片，才能将完整的数据包交给内核 NAT 功能。

> **注意**
>
> 此示例中使用的 NAT 实例和规则号与 **rc.firewall** 创建的默认 NAT 实例和规则号不匹配。**rc.firewall** 是设置 FreeBSD 默认防火墙规则的脚本。

```ini
$cmd 005 allow all from any to any via em1  # 允许局域网流量直通
$cmd 010 allow all from any to any via lo0  # 允许回环接口流量
$cmd 099 reass all from any to any in       # 重新组装入站数据包，便于 NAT 处理
$cmd 100 nat 1 ip from any to any in via $pif # 对入站到外部接口的流量执行 NAT
# 检查动态规则表，如已存在会话则直接通过
$cmd 101 check-state
```

出站规则被修改，使用 `$skip` 变量替换 `allow` 动作，表示规则处理将在规则 `1000` 处继续。多条 `tcp` 规则已被规则 `125` 替换，因为 `$good_tcpo` 变量包含了允许的出站端口列表。

> **注意**
>
> IPFW 的性能在很大程度上取决于规则集中规则的数量。

```ini
# 允许出站的 DNS 查询（TCP 与 UDP）
$cmd 120 $skip udp from any to x.x.x.x 53 out via $pif $ks
# 允许出站的 DHCP 请求
$cmd 121 $skip udp from any to x.x.x.x 67 out via $pif $ks
# 合并允许的出站 TCP 端口（SSH、SMTP、DNS、HTTP、HTTPS），使用 skipto 跳转至 NAT 规则
$cmd 125 $skip tcp from any to any $good_tcpo out via $pif setup $ks
# 允许出站的 ICMP（如 ping 与 traceroute）
$cmd 130 $skip icmp from any to any out via $pif $ks
```

入站规则除最后一条外保持不变，该规则删除了 `via $pif`，以便捕获入站和出站规则。NAT 规则必须跟在最后一条出站规则后面，规则号必须大于最后一条规则，并且规则号必须通过 `skipto` 动作引用。在此规则集中，规则号 `1000` 将所有数据包传递到所配置的 NAT 实例处理。下一条规则允许任何经过 NAT 处理的数据包通过。

```ini
# 拒绝并记录所有未匹配规则的流量
$cmd 999 deny log all from any to any
# 出站流量 skipto 的目标：对所有未匹配到具体规则的出站流量执行 NAT
$cmd 1000 nat 1 ip from any to any out via $pif
# 允许经过 NAT 转换后的流量通过
$cmd 1001 allow ip from any to any
```

在此示例中，规则 `100`、`101`、`125`、`1000` 和 `1001` 控制出站和入站数据包的地址转换，从而确保动态状态表中的条目始终注册为私有 LAN IP 地址。

假设有一台内部主机使用网页浏览器，发起了一个新的出站 HTTP 会话，使用端口 80。当第一个出站数据包进入防火墙时，它不会匹配规则 `100`，因为它是出站的，而不是入站的。它会通过规则 `101`，因为这是第一个数据包，并且它尚未被添加到动态状态表中。数据包最终会匹配规则 `125`，因为它是出站的，并且它的源 IP 地址来自内部 LAN。当它匹配这个规则时，会执行两个动作。首先，`keep-state` 动作将一个条目添加到动态状态表中，并执行指定的动作 `skipto rule 1000`。接下来，数据包会经过 NAT 处理并被发送到互联网。这个数据包到达目标 Web 服务器，生成并返回响应数据包。这个新数据包进入规则集的顶部。它匹配规则 `100`，并将目标 IP 地址映射回原始的内部地址。接着它会通过 `check-state` 规则，被识别为已有会话的数据包，并被放行到 LAN。

在入站方面，规则集必须拒绝不合法的数据包，只允许授权的服务。一个匹配入站规则的数据包会被记录到动态状态表中，然后该数据包会被放行到 LAN。作为响应生成的数据包，会被 `check-state` 规则识别为属于一个已存在的会话，然后发送到规则 `1000`，经过 NAT 后再放行到出站接口。

### 端口重定向

NAT 的一个缺点是，LAN 客户端无法从互联网访问。LAN 上的客户端可发起外向连接，但无法接收传入连接。若尝试在 LAN 客户机上运行互联网服务，则会成为问题。解决此问题的一种简单方法是将选定的互联网端口重定向到 NAT 提供机上的 LAN 客户机。

例如，Minecraft 服务器在客户端 `A` 上运行，Web 服务器在客户端 `B` 上运行。要使其正常工作，接收到端口 25565（Minecraft）和 80（HTTP）上的连接必须重定向到相应的机器。

在内核态 NAT 中，所有配置都在 NAT 实例配置中完成。`redirect_port` 的语法如下：

```ini
# 语法：redirect_port 协议 内部 IP:内部端口 [-内部端口] [外部 IP:] 外部端口 [-外部端口] [远程 IP[:远程端口 [-远程端口]]]
redirect_port proto targetIP:targetPORT[-targetPORT]
  [aliasIP:]aliasPORT[-aliasPORT]
  [remoteIP[:remotePORT[-remotePORT]]]
```

要配置上述示例，参数应为：

```sh
# 将外部 25565 端口重定向到内部 Minecraft 服务器
redirect_port tcp 192.168.0.2:25565 25565
# 将外部 80 端口重定向到内部 Web 服务器
redirect_port tcp 192.168.0.3:80 80
```

将这些参数添加到上面规则集中 NAT 实例 1 的配置中后，TCP 端口将转发至运行 Minecraft 和 HTTP 服务的局域网客户机。

```sh
# 在 NAT 实例 1 中添加端口重定向规则
ipfw -q nat 1 config if $pif same_ports unreg_only reset redirect_port tcp 192.168.0.2:25565 25565 redirect_port tcp 192.168.0.3:80 80
```

通过 `redirect_port`，还可指定端口范围而非单个端口。例如，`tcp 192.168.0.2:2000-3000 2000-3000` 将把所有接收到的 2000 到 3000 端口上的连接转发到客户端 `A` 上的 2000 到 3000 端口。

### 地址重定向

在可用多个 IP 地址时，地址重定向非常有用。可通过 ipfw(8) 为局域网中的每台客户机分配其专有的外部 IP 地址，之后将重写来自局域网客户机的出站数据包，使用正确的外部 IP 地址，并将所有传入该 IP 地址的流量重定向回特定的局域网客户机。这也称为静态 NAT。

例如，若有 IP 地址 **128.1.1.1**、**128.1.1.2** 和 **128.1.1.3** 可用：**128.1.1.1** 可作为 ipfw(8) 机器的外部 IP 地址，而 **128.1.1.2** 和 **128.1.1.3** 则分别转发到局域网客户机 `A` 和 `B`：

```sh
Internet ──► [ ipfw ] ──┬─► 128.1.1.2 ──► 局域网客户机 A
             │          │
             │          └─► 128.1.1.3 ──► 局域网客户机 B
             └── (外部地址: 128.1.1.1)
```

`redirect_addr` 的语法如下，其中 `localIP` 是局域网客户机的内部 IP 地址，`publicIP` 是与局域网客户机对应的外部 IP 地址。

```sh
# 语法：redirect_addr 内部 IP 外部 IP（将内部客户机静态映射到某个公网地址）
redirect_addr localIP publicIP
```

在此示例中，参数应为：

```sh
# 将内部主机 192.168.0.2 静态映射到外部地址 128.1.1.2
redirect_addr 192.168.0.2 128.1.1.2
# 将内部主机 192.168.0.3 静态映射到外部地址 128.1.1.3
redirect_addr 192.168.0.3 128.1.1.3
```

与 `redirect_port` 类似，这些参数应放在 NAT 实例的配置中。使用地址重定向时，无需端口重定向，因为所有接收到的特定 IP 地址上的数据都会被重定向。

ipfw(8) 机器上的外部 IP 地址必须处于活动状态，且已作为别名配置在外部接口上。

## IPFW 命令

`ipfw` 可用于在防火墙运行时手动添加或删除单个规则。使用这种方法的问题在于，所有更改在系统重启时都会丢失。因此，建议将所有规则写入一个文件，并在引导时使用该文件加载规则，且在该文件发生更改时替换当前运行的防火墙规则。

`ipfw` 可将运行中的防火墙规则显示到控制台屏幕。IPFW 计数功能会动态为每条规则创建一个计数器，用于统计每个匹配该规则的数据包数。在测试规则时，列出带有计数器的规则可确定规则是否按预期工作。

按顺序列出所有正在运行的规则：

```sh
# ipfw list
```

列出所有正在运行的规则，并带有规则上次匹配的时间戳：

```sh
# ipfw -t list
```

下一示例列出了计数信息和匹配规则的数据包计数，以及规则本身。第一列是规则编号，接着是匹配的数据包和字节数，再后面是规则本身。

```sh
# ipfw -a list
```

除了静态规则外，列出动态规则：

```sh
# ipfw -d list
```

同时显示已过期的动态规则：

```sh
# ipfw -d -e list
```

清零计数器：

```sh
# ipfw zero
```

仅清零编号为 **NUM** 的规则的计数器：

```sh
# ipfw zero NUM
```

### 记录防火墙日志

即使启用了日志功能，IPFW 默认不会生成任何规则日志。防火墙管理员决定哪些规则在规则集中将被记录，并将关键字 `log` 添加到这些规则中。通常，只有拒绝规则才会被记录。习惯上，将“ipfw 默认拒绝所有”规则复制一遍，并在规则集的最后加上 `log` 关键字，以便查看所有未匹配规则集中任何规则的数据包。

日志记录是双刃剑。若不谨慎，过多的日志数据或 DoS 攻击会填满磁盘。日志消息不仅会写入 syslogd，还会显示在根控制台上，且很快会令人困扰。

内核选项 `IPFIREWALL_VERBOSE_LIMIT=5` 限制了发送到 syslogd(8) 的关于某一规则匹配的数据包消息的连续次数。当此选项在内核中启用时，关于某个特定规则的连续消息数量将限制为指定的数量。也可通过 sysctl `net.inet.ip.fw.verbose_limit` 在运行时设置此值，从而无需重编译内核。从 200 条相同的日志消息中无法获取任何有用信息。将此选项设置为 5 时，关于特定规则的五条连续消息会被记录到 syslogd，剩余的相同连续消息会被计数并发送到 syslogd，附带类似以下的短语：

```sh
last message repeated 45 times
```

所有记录的数据包消息默认写入 **/var/log/security**，在 **/etc/syslog.conf** 中定义该路径。

### 构建规则脚本

大多数经验丰富的 IPFW 用户会创建一个包含规则的文件，并编写为可执行的脚本。这样做的主要好处是防火墙规则可批量刷新，无需重启系统即可激活。这种方法在测试新规则时非常方便，因为该过程可根据需要多次执行。作为脚本，可使用符号替代多个规则中频繁使用的值。

此示例脚本与 sh(1)、csh(1) 和 tcsh(1) shell 使用的语法兼容。符号替代字段以美元符号（`$`）为前缀。符号字段没有 `$` 前缀。要填充符号字段的值必须用双引号（`"`）括起来。

以如下方式启动规则文件：

```ini
# 清空既有规则
ipfw -q -f flush
# 设置变量
oif="tun0" # 外网接口
odns="192.0.2.11" # ISP 的 DNS 服务器 IP 地址
cmd="ipfw -q add " # 构建规则前缀
ks="keep-state" # 有状态跟踪，避免每次重复输入
$cmd 00500 check-state # 检查动态规则表
$cmd 00502 deny all from any to any frag # 拒绝分片
$cmd 00501 deny tcp from any to any established # 拒绝未建立会话的 ACK
$cmd 00600 allow tcp from any to any 80 out via $oif setup $ks # 允许出站 HTTP
$cmd 00610 allow tcp from any to $odns 53 out via $oif setup $ks # 允许出站 DNS（TCP）
$cmd 00611 allow udp from any to $odns 53 out via $oif $ks # 允许出站 DNS（UDP）
```

这些规则并非重点，因为此示例旨在演示符号替代字段的填充方法。

若上述示例位于 **/etc/ipfw\.rules**，可通过以下命令重新加载规则：

```sh
# sh /etc/ipfw.rules
```

**/etc/ipfw\.rules** 可位于任何地方，也可以任意指定文件名。

同样，也可以手动执行以下命令：

```sh
# ipfw -q -f flush
# ipfw -q add check-state
# ipfw -q add deny all from any to any frag
# ipfw -q add deny tcp from any to any established
# ipfw -q add allow tcp from any to any 80 out via tun0 setup keep-state
# ipfw -q add allow tcp from any to 192.0.2.11 53 out via tun0 setup keep-state
# ipfw -q add 00611 allow udp from any to 192.0.2.11 53 out via tun0 keep-state
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://book.bsdcn.org/di-29-zhang-fang-huo-qiang/di-29.2-jie-ipfirewall-ipfw.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
