# 33.4.IPFW

IPFW 是一个为 FreeBSD 编写的有状态防火墙，支持 IPv4 和 IPv6。它由多个组件组成：内核防火墙过滤规则处理器及其集成的数据包计数设施、日志设施、NAT、[dummynet(4)](https://man.freebsd.org/cgi/man.cgi?query=dummynet\&sektion=4\&format=html) 流量整形器、转发设施、桥接设施和 ipstealth 设施。

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

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

## 33.4.1. 启用 IPFW

IPFW 已包含在 FreeBSD 的基本安装中，作为一个内核可加载模块，这意味着无需自定义内核即可启用 IPFW。

对于希望将 IPFW 支持静态编译到自定义内核的用户，请参阅 [IPFW 内核选项](https://docs.freebsd.org/en/books/handbook/firewalls/#firewalls-ipfw-kernelconfig)。

要配置系统在启动时启用 IPFW，请在 **/etc/rc.conf** 中添加 `firewall_enable="YES"`：

```sh
# sysrc firewall_enable="YES"
```

要使用 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)](https://man.freebsd.org/cgi/man.cgi?query=syslogd\&sektion=8\&format=html) 启用日志记录，请包括这一行：

```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
```

## 33.4.2. IPFW 规则语法

当数据包进入 IPFW 防火墙时，它会与规则集中的第一条规则进行比较，并按顺序逐条规则进行处理，从上到下。当数据包与某条规则的选择条件匹配时，该规则的动作会被执行，规则集的搜索就会终止。这个过程称为“第一个匹配的规则生效”。如果数据包没有与任何规则匹配，它会被强制性 IPFW 默认规则 65535 捕获，该规则会拒绝所有数据包并悄悄丢弃它们。然而，如果数据包匹配了包含 `count`、`skipto` 或 `tee` 关键字的规则，搜索会继续进行。有关这些关键字如何影响规则处理的详细信息，请参阅 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。

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

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

本节提供了这些关键字及其选项的概述，并非每个可能选项的详尽列表。有关创建 IPFW 规则时可用规则语法的完整描述，请参阅 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。

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

  `allow | accept | pass | permit`：这些关键字是等价的，允许匹配规则的数据包。

  `check-state`：检查数据包与动态状态表的匹配情况。如果找到匹配项，则执行与生成此动态规则的规则关联的动作，否则移动到下一条规则。如果规则集中没有 `check-state` 规则，则会在第一个 `keep-state` 或 `limit` 规则处检查动态规则表。

  `count`：更新匹配规则的所有数据包计数器。搜索会继续进行到下一条规则。

  `deny | drop`：这两个词会悄悄丢弃匹配规则的数据包。

  更多动作选项，请参考 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。
* **LOG\_AMOUNT**\
  当数据包匹配包含 `log` 关键字的规则时，将通过 [syslogd(8)](https://man.freebsd.org/cgi/man.cgi?query=syslogd\&sektion=8\&format=html) 记录一条消息，设施名称为 `SECURITY`。日志记录仅在特定规则记录的数据包数量没有超过指定的 `LOG_AMOUNT` 时发生。如果没有指定 `LOG_AMOUNT`，则限制值将取自 `net.inet.ip.fw.verbose_limit` 的值。设置为零将移除日志记录限制。待达到限制，日志可以通过清除该规则的日志计数器或数据包计数器来重新启用，使用 `ipfw resetlog`。

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

* **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)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。

## 33.4.3. 示例规则集

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

> **注意**
>
> 在首次创建或测试防火墙规则集时，考虑暂时设置这个可调参数：
>
> ```
> net.inet.ip.fw.default_to_accept="1"
> ```
>
> 这将把 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html) 的默认策略设置为比默认的 `deny ip from any to any` 更宽松，这样可以在重启后稍微降低被锁定的风险。

防火墙脚本首先声明它是一个 Bourne shell 脚本，并清空所有现有规则。然后它创建了 `cmd` 变量，以便在每条规则开始时不必重复输入 `ipfw add`。它还定义了 `pif` 变量，表示连接到 Internet 的接口名称。

```sh
#!/bin/sh
# 在开始之前清空现有规则。
ipfw -q -f flush

# 设置规则命令前缀
cmd="ipfw -q add"
pif="dc0"     # 连接到 Internet 的 NIC 接口名称
```

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

```sh
# 将 xl0 改为 LAN NIC 接口名称
$cmd 00005 allow all from any to any via xl0

# 环回接口没有任何限制
$cmd 00010 allow all from any to any via lo0
```

接下来的规则允许数据包通过，如果它与动态规则表中的现有条目匹配：

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

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

```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 服务器，用于电缆/DSL 配置
# 使用第一条规则并检查日志中的 IP 地址
# 然后取消注释第二条规则，输入 IP 地址并删除第一条规则
$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

# 允许外发邮件连接
$cmd 00230 allow tcp from any to any 25 out via $pif setup keep-state
$cmd 00231 allow tcp from any to any 110 out via $pif setup keep-state

# 允许外发 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
```

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

```ini
# 拒绝所有来自不可路由保留地址空间的入站流量
$cmd 00300 deny all from 192.168.0.0/16 to any in via $pif     #RFC 1918 私有 IP
$cmd 00301 deny all from 172.16.0.0/12 to any in via $pif      #RFC 1918 私有 IP
$cmd 00302 deny all from 10.0.0.0/8 to any in via $pif         #RFC 1918 私有 IP
$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     #DHCP 自动配置
$cmd 00306 deny all from 192.0.2.0/24 to any in via $pif       #保留地址，用于文档
$cmd 00307 deny all from 204.152.64.0/23 to any in via $pif    #Sun 集群互联
$cmd 00308 deny all from 224.0.0.0/3 to any in via $pif        #D 类和 E 类组播

# 拒绝公共 ping
$cmd 00310 deny icmp from any to any in via $pif

# 拒绝 ident
$cmd 00315 deny tcp from any to any 113 in via $pif

# 拒绝所有 Netbios 服务。
$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 81 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 中使用的相同 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
```

## 33.4.4. 内核 NAT

FreeBSD 的 IPFW 防火墙有两种 NAT 实现方式：用户空间实现的 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 和较新的内核 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"
skip="skipto 1000"
pif=dc0
ks="keep-state"
good_tcpo="22,25,37,53,80,443,110"
```

使用内核 NAT 时，由于 [libalias(3)](https://man.freebsd.org/cgi/man.cgi?query=libalias\&sektion=3\&format=html) 的架构，需要禁用 TCP 分段卸载（TSO），libalias 是作为内核模块实现的库，用于提供 IPFW 的内核 NAT 功能。可以使用 [ifconfig(8)](https://man.freebsd.org/cgi/man.cgi?query=ifconfig\&sektion=8\&format=html) 按每个网络接口禁用 TSO，也可以使用 [sysctl(8)](https://man.freebsd.org/cgi/man.cgi?query=sysctl\&sektion=8\&format=html) 全局禁用 TSO。要全局禁用 TSO，必须在 **/etc/sysctl.conf** 中设置以下内容：

```sh
net.inet.tcp.tso="0"
```

还将配置一个 NAT 实例。可以有多个 NAT 实例，每个实例都有自己的配置。此示例中只需要一个 NAT 实例，即 NAT 实例 1。配置可以包括几个选项，如：`if` 指定公共接口，`same_ports` 确保别名端口和本地端口号保持一致，`unreg_only` 仅处理未注册（私有）地址空间，`reset` 可帮助即使 IPFW 机器的公共 IP 地址发生变化时，保持 NAT 实例正常工作。有关可以传递给单个 NAT 实例配置的所有可能选项，请查阅 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。配置有状态 NAT 防火墙时，需要允许翻译后的数据包重新注入防火墙进行进一步处理。这可以通过在防火墙脚本开始时禁用 `one_pass` 行为来实现。

```sh
ipfw disable one_pass
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 功能。

> **注意**
>
> 使用用户空间的 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 时，不需要重新组装规则，因为 IPFW 的 `divert` 动作的内部工作机制已经在将数据包传递到套接字之前负责重新组装数据包，这一点在 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html) 中也有说明。此示例中使用的 NAT 实例和规则号与 **rc.firewall** 创建的默认 NAT 实例和规则号不匹配。**rc.firewall** 是一个设置 FreeBSD 默认防火墙规则的脚本。

```ini
$cmd 005 allow all from any to any via xl0  # 排除 LAN 流量
$cmd 010 allow all from any to any via lo0  # 排除环回流量
$cmd 099 reass all from any to any in       # 重新组装入站数据包
$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
# 授权的出站数据包
$cmd 120 $skip udp from any to x.x.x.x 53 out via $pif $ks
$cmd 121 $skip udp from any to x.x.x.x 67 out via $pif $ks
$cmd 125 $skip tcp from any to any $good_tcpo out via $pif setup $ks
$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
$cmd 1000 nat 1 ip from any to any out via $pif # 跳转位置，用于出站有状态规则
$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 后再释放到出站接口。

> **注意**
>
> 从用户态的 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 转向内核态 NAT 可能看起来非常平滑，但实际上有一个小陷阱。当使用 GENERIC 内核时，启用 `firewall_nat_enable` 后，IPFW 会加载 **libalias.ko** 内核模块。**libalias.ko** 内核模块仅提供基本的 NAT 功能，而用户态实现的 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 则在其用户态库中提供了所有 NAT 功能，且无需额外配置。所有功能指的是除了标准的 **libalias.ko** 内核模块外，还可以根据需要加载的内核模块：**alias\_ftp.ko**、**alias\_bbt.ko**、**skinny.ko**、**irc.ko**、**alias\_pptp.ko** 和 **alias\_smedia.ko**，这些都可以通过 **/etc/rc.conf** 中的 `kld_list` 指令加载。如果使用的是自定义内核，可以通过 `options LIBALIAS` 将用户态库的全部功能编译到内核中。

### 33.4.4.1. 端口重定向

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

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

在内核态 NAT 中，所有配置都在 NAT 实例配置中完成。有关内核态 NAT 实例可以使用的所有选项的完整列表，请参阅 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html)。IPFW 的语法遵循 natd 的语法。`redirect_port` 的语法如下：

```ini
redirect_port proto targetIP:targetPORT[-targetPORT]
  [aliasIP:]aliasPORT[-aliasPORT]
  [remoteIP[:remotePORT[-remotePORT]]]
```

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

```sh
redirect_port tcp 192.168.0.2:6667 6667
redirect_port tcp 192.168.0.3:80 80
```

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

```sh
ipfw -q nat 1 config if $pif same_ports unreg_only reset \
  redirect_port tcp 192.168.0.2:6667 6667 \
  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 端口。

### 33.4.4.2. 地址重定向

地址重定向在可用多个 IP 地址时非常有用。每个 LAN 客户机可以通过 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html) 分配其专有的外部 IP 地址，之后将重写来自 LAN 客户机的出站数据包，使用正确的外部 IP 地址，并将所有传入该 IP 地址的流量重定向回特定的 LAN 客户机。这也称为静态 NAT。例如，如果有 IP 地址 `128.1.1.1`、`128.1.1.2` 和 `128.1.1.3` 可用，`128.1.1.1` 可以作为 [ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html) 机器的外部 IP 地址，而 `128.1.1.2` 和 `128.1.1.3` 则分别转发到 LAN 客户机 `A` 和 `B`。

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

```sh
redirect_addr localIP publicIP
```

在此示例中，参数应为：

```sh
redirect_addr 192.168.0.2 128.1.1.2
redirect_addr 192.168.0.3 128.1.1.3
```

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

[ipfw(8)](https://man.freebsd.org/cgi/man.cgi?query=ipfw\&sektion=8\&format=html) 机器上的外部 IP 地址必须是活动的，并且已别名到外部接口上。有关详细信息，请参阅 [rc.conf(5)](https://man.freebsd.org/cgi/man.cgi?query=rc.conf\&sektion=5\&format=html)。

### 33.4.4.3. 用户态 NAT

首先声明：用户态 NAT 实现 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 比内核态 NAT 更具开销。为了让 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 翻译数据包，这些数据包必须从内核复制到用户态再返回，这带来了额外的开销，而内核态 NAT 没有这种问题。

要在启动时启用用户态 NAT 守护进程 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html)，以下是 **/etc/rc.conf** 中的最低配置。`natd_interface` 设置为连接到互联网的 NIC 的名称。 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 的 [rc(8)](https://man.freebsd.org/cgi/man.cgi?query=rc\&sektion=8\&format=html) 脚本会自动检查是否使用动态 IP 地址，并配置自身来处理。

```sh
gateway_enable="YES"
natd_enable="YES"
natd_interface="rl0"
```

通常，上述为内核态 NAT 所解释的规则集也可以与 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html) 一起使用。例外情况是内核态 NAT 实例的配置 `(ipfw -q nat 1 config …)`，与重新组装规则 99 一起使用时不需要，因为它的功能已包含在 `divert` 动作中。规则号 100 和 1000 需要稍微更改，如下所示。

```sh
$cmd 100 divert natd ip from any to any in via $pif
$cmd 1000 divert natd ip from any to any out via $pif
```

要配置Port 或地址重定向，可以使用类似于内核态 NAT 的语法。尽管现在，在规则集脚本中指定配置不像内核态 NAT 那样进行，而是最好的做法是在配置文件中配置 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html)。为此，必须通过 **/etc/rc.conf** 传递一个额外的标志，指定配置文件的路径。

```sh
natd_flags="-f /etc/natd.conf"
```

> **注意**
>
> 指定的文件必须包含每行一个配置选项。有关配置文件和可能的变量的更多信息，请参阅 [natd(8)](https://man.freebsd.org/cgi/man.cgi?query=natd\&sektion=8\&format=html)。以下是两个示例条目，每行一个：
>
> ```ini
> redirect_port tcp 192.168.0.2:6667 6667
> redirect_addr 192.168.0.3 128.1.1.3
> ```

## 33.4.5. 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
```

### 33.4.5.1. 记录防火墙日志

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

日志记录是双刃剑。如果不小心，过多的日志数据或 DoS 攻击会将磁盘填满日志文件。日志消息不仅会写入 syslogd，还会显示在根控制台上，并且很快会变得烦人。

`IPFIREWALL_VERBOSE_LIMIT=5` 内核选项限制了发送到 [syslogd(8)](https://man.freebsd.org/cgi/man.cgi?query=syslogd\&sektion=8\&format=html) 的关于某一规则匹配的数据包消息的连续次数。当此选项在内核中启用时，关于某个特定规则的连续消息数量将限制为指定的数量。从 200 条相同的日志消息中无法获取任何有用信息。将此选项设置为 5 时，关于特定规则的五条连续消息会被记录到 syslogd，剩余的相同连续消息会被计数并发布到 syslogd，附带类似以下的短语：

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

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

### 33.4.5.2. 构建规则脚本

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

这个示例脚本与 [sh(1)](https://man.freebsd.org/cgi/man.cgi?query=sh\&sektion=1\&format=html)、[csh(1)](https://man.freebsd.org/cgi/man.cgi?query=csh\&sektion=1\&format=html) 和 [tcsh(1)](https://man.freebsd.org/cgi/man.cgi?query=tcsh\&sektion=1\&format=html) shell 使用的语法兼容。符号替代字段以美元符号（$）为前缀。符号字段没有 $ 前缀。要填充符号字段的值必须用双引号（"）括起来。

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

```ini
##########  ipfw 示例脚本开头 #########
#
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
$cmd 00600 allow tcp from any to any 80 out via $oif setup $ks
$cmd 00610 allow tcp from any to $odns 53 out via $oif setup $ks
$cmd 00611 allow udp from any to $odns 53 out via $oif $ks
############# End of example ipfw rules script ########
```

这些规则不重要，因为这个示例的重点是如何填充符号替代字段。

如果上述示例位于 **/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
```

## 33.4.6. IPFW 内核选项

为了将 IPFW 支持静态编译到自定义内核中，请参考 [Configuring the FreeBSD Kernel](https://docs.freebsd.org/en/books/handbook/kernelconfig/#kernelconfig) 中的说明。以下选项可用于自定义内核配置文件：

```ini
options    IPFIREWALL			# 启用 IPFW
options    IPFIREWALL_VERBOSE		# 启用带有 log 关键字的规则的日志记录到 syslogd(8)
options    IPFIREWALL_VERBOSE_LIMIT=5	# 限制每个条目记录的数据包数量
options    IPFIREWALL_DEFAULT_TO_ACCEPT # 设置默认策略为通过未明确拒绝的数据包
options    IPFIREWALL_NAT		# 启用基本的内核 NAT 支持
options    LIBALIAS			# 启用完整的内核 NAT 支持
options    IPFIREWALL_NAT64		# 启用内核 NAT64 支持
options    IPFIREWALL_NPTV6		# 启用内核 IPv6 NPT 支持
options    IPFIREWALL_PMOD		# 启用协议修改模块支持
options    IPDIVERT			# 启用通过 natd(8) 进行 NAT
```

> **注意**
>
> IPFW 可以作为内核模块加载：上述选项默认作为模块构建，或者可以使用 tunables 在运行时设置。
