> 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-34-zhang-fang-huo-qiang/34.3.-pf.md).

# 34.3.PF

自 FreeBSD 5.3 以来，OpenBSD 的 PF 防火墙移植版本已作为基本系统的集成部分包含在内。PF 是完整的、功能丰富的防火墙，可选支持 ALTQ（Alternate Queuing），提供服务质量（QoS）。

OpenBSD 项目维护着 PF 的权威参考文档，详见 [PF FAQ](http://www.openbsd.org/faq/pf/)。Peter Hansteen 维护了一篇详细的 PF 教程，网址为 <http://home.nuug.no/~peter/pf/>。

> **警告**
>
> 在阅读 [PF FAQ](http://www.openbsd.org/faq/pf/) 时，请注意 FreeBSD 版本的 PF 与上游的 OpenBSD 版本多年来已存在显著差异。并非所有功能在 FreeBSD 上的表现都与 OpenBSD 中相同，反之亦然。

[FreeBSD 包过滤邮件列表](https://lists.freebsd.org/subscription/freebsd-pf) 是一个询问关于配置和运行 PF 防火墙问题的好地方。在提问之前，请先查阅邮件列表存档，因为问题可能已得到解答。

本部分手册专注于 PF 在 FreeBSD 中的使用，演示了如何启用 PF 和 ALTQ，并提供了几个在 FreeBSD 系统上创建规则集的示例。

## 34.3.1. 启用 PF

要使用 PF，必须首先加载其内核模块。本节说明了可以添加到 **/etc/rc.conf** 中的条目，以启用 PF。

首先，向 **/etc/rc.conf** 添加 `pf_enable=yes`：

```sh
# sysrc pf_enable=yes
```

还可以在启动 PF 时传递其他选项，详见 [pfctl(8)](https://man.freebsd.org/cgi/man.cgi?query=pfctl\&sektion=8\&format=html)。在 **/etc/rc.conf** 中添加或更改此条目，并在引号（`""`）之间指定任何需要的标志：

```sh
pf_flags=""                     # pfctl 启动时的附加标志
```

PF 找不到规则集配置文件时将无法启动。默认情况下，FreeBSD 不带规则集，也没有 **/etc/pf.conf** 文件。示例规则集位于 **/usr/share/examples/pf/**。如果自定义规则集保存在其他地方，可在 **/etc/rc.conf** 中添加一行，指定文件的完整路径：

```sh
pf_rules="/path/to/pf.conf"
```

PF 的日志支持由 [pflog(4)](https://man.freebsd.org/cgi/man.cgi?query=pflog\&sektion=4\&format=html) 提供。要启用日志支持，请在 **/etc/rc.conf** 中添加 `pflog_enable=yes`：

```sh
# sysrc pflog_enable=yes
```

还可以添加以下行来更改日志文件的默认位置，或指定启动 [pflog(4)](https://man.freebsd.org/cgi/man.cgi?query=pflog\&sektion=4\&format=html) 时要传递的附加标志：

```sh
pflog_logfile="/var/log/pflog"  # pflogd 应该存储日志文件的位置
pflog_flags=""                  # pflogd 启动时的附加标志
```

最后，如果防火墙后面有局域网，并且需要转发数据包给局域网中的计算机，或需要 NAT，启用以下选项：

```sh
gateway_enable="YES"            # 启用作为 LAN 网关
```

保存必要的编辑后，可通过以下命令启动 PF，并启用日志支持：

```sh
# service pf start
# service pflog start
```

默认情况下，PF 从 **/etc/pf.conf** 读取其配置规则，并根据该文件中指定的规则或定义来修改、丢弃或放行数据包。FreeBSD 安装包括位于 **/usr/share/examples/pf/** 中的几个示例文件。有关 PF 规则集的完整内容，请参阅 [PF FAQ](http://www.openbsd.org/faq/pf/)。

要控制 PF，可使用 `pfctl`。有用的 `pfctl` 选项总结了该命令的一些常用选项。有关所有可用选项的说明，请参阅 [pfctl(8)](https://man.freebsd.org/cgi/man.cgi?query=pfctl\&sektion=8\&format=html)：

**表 1. 有用的 `pfctl` 选项**

| 命令                                    | 作用                                         |
| ------------------------------------- | ------------------------------------------ |
| `pfctl -e`                            | 启用 PF。                                     |
| `pfctl -d`                            | 禁用 PF。                                     |
| `pfctl -F all -f /etc/pf.conf`        | 清除所有 NAT、过滤、状态和表规则，并重新加载 **/etc/pf.conf**。 |
| `pfctl -s [ rules \| nat \| states ]` | 报告过滤规则、NAT 规则或状态表。                         |
| `pfctl -vnf /etc/pf.conf`             | 检查 **/etc/pf.conf** 中的错误，但不加载规则集。          |

> **技巧**
>
> [security/sudo](https://cgit.freebsd.org/ports/tree/security/sudo/) 对于运行需要提升权限的命令（如 `pfctl`）非常有用，可以通过 Ports 安装。

为监控通过 PF 防火墙的流量，可考虑安装软件包或 Port [sysutils/pftop](https://cgit.freebsd.org/ports/tree/sysutils/pftop/)。安装后，可运行 pftop 查看流量的实时快照，其格式类似于 [top(1)](https://man.freebsd.org/cgi/man.cgi?query=top\&sektion=1\&format=html)。

## 34.3.2. PF 规则集

本节演示了如何创建自定义规则集。从最简单的规则集开始，逐步构建其概念，并通过几个示例展示 PF 众多功能的实际应用。

最简单的规则集适用于不运行任何服务、只需要访问一个网络（可能是互联网）的单一计算机。要创建此最小规则集，请编辑 **/etc/pf.conf**，使其内容如下：

```sh
block in all
pass out all keep state
```

第一条规则默认拒绝所有传入的流量。第二条规则允许由该系统发起的连接通过，同时保留这些连接的状态信息。这些状态信息允许该连接的返回流量通过，并且应仅在信任的计算机上使用。可以通过以下命令加载规则集：

```sh
# pfctl -e ; pfctl -f /etc/pf.conf
```

除了保持状态外，PF 还提供了 *列表* 和 *宏*，可在创建规则时使用。宏可以包含列表，且需要在使用之前定义。例如，在规则集的顶部插入以下行：

```sh
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"
udp_services = "{ domain }"
```

PF 理解端口名以及端口号，只要这些名称在 **/etc/services** 中列出。此示例创建了两个宏，第一个是七个 TCP 端口名的列表，第二个是一个 UDP 端口名。定义后，宏就可以在规则中使用。在此示例中，除了该系统发起的七个指定的 TCP 服务连接和一个指定的 UDP 服务连接外，所有流量都会被阻止：

```sh
tcp_services = "{ ssh, smtp, domain, www, pop3, auth, pop3s }"
udp_services = "{ domain }"
block all
pass out proto tcp to any port $tcp_services keep state
pass proto udp to any port $udp_services keep state
```

尽管 UDP 被认为是无状态协议，但 PF 能跟踪某些状态信息。例如，传递 UDP 请求查询域名服务器时，PF 会监控返回流量，允许其通过。

每当编辑规则集时，必须加载新规则才能使用：

```sh
# pfctl -f /etc/pf.conf
```

如果没有语法错误，`pfctl` 在加载规则时不会输出任何消息。也可以在尝试加载规则之前先测试：

```sh
# pfctl -nf /etc/pf.conf
```

包括 `-n` 选项会让规则仅解释而不加载。这样就有机会纠正任何错误。在任何时候，最后加载的有效规则集都会被强制执行，直到 PF 被禁用或加载新规则集为止。

> **技巧**
>
> 在 `pfctl` 规则集验证或加载时添加 `-v` 选项，会显示完全解析后的规则，准确展示它们的加载方式。这在调试规则时非常有用。

### 34.3.2.1. 简单的网关与 NAT

本节演示如何配置运行 PF 的 FreeBSD 系统，使其作为至少一台其他计算机的网关。网关需要至少两个网络接口，每个接口连接到不同的网络。在此示例中，`xl0` 连接到互联网，`xl1` 连接到内部网络。

首先，启用网关，让机器转发它在一个接口上接收到的网络流量到另一个接口。此 sysctl 设置将转发 IPv4 数据包：

```sh
# sysctl net.inet.ip.forwarding=1
```

要转发 IPv6 流量，使用：

```sh
# sysctl net.inet6.ip6.forwarding=1
```

要在系统引导时启用这些设置，使用 [sysrc(8)](https://man.freebsd.org/cgi/man.cgi?query=sysrc\&sektion=8\&format=html) 将它们添加到 **/etc/rc.conf** 中：

```sh
# sysrc gateway_enable=yes
# sysrc ipv6_gateway_enable=yes
```

使用 `ifconfig` 验证两个接口是否已启动并正常运行。

接下来，创建 PF 规则以允许网关转发流量。以下规则允许来自内部网络主机的有状态流量通过网关，但 `to` 关键字并不保证数据包会从源头到目的地完全通过：

```sh
pass in on xl1 from xl1:network to xl0:network port $ports keep state
```

该规则仅允许流量通过内部接口进入网关。为让数据包进一步传递，还需要一个匹配的规则：

```sh
pass out on xl0 from xl1:network to xl0:network port $ports keep state
```

虽然这两个规则有效，但这种特定的规则通常不需要。对忙碌的网络管理员而言，可读的规则集才是更安全的规则集。本节其余部分演示了如何使规则尽可能简单以便于阅读。例如，这两个规则可用一条规则替代：

```sh
pass from xl1:network to any port $ports keep state
```

`interface:network` 表示法可以替换为宏，以使规则集更具可读性。例如，可定义 `$localnet` 宏，表示直接连接到内部接口的网络（`$xl1:network`）。或者，`$localnet` 的定义可以更改为 *IP 地址/子网掩码* 表示法，来表示网络，例如 **192.168.100.1/24** 用于表示一个私有地址的子网。

如果需要，`$localnet` 甚至可以定义为网络列表。不论具体需求如何，合理的 `$localnet` 定义都可在典型的通过规则中使用，如下所示：

```sh
pass from $localnet to any port $ports keep state
```

以下示例规则集允许所有由内部网络机器发起的流量。它首先定义两个宏，表示网关的外部和内部 3COM 接口。

> **注意**
>
> 对于拨号用户，外部接口将使用 `tun0`。对于 ADSL 连接，特别是那些使用 PPPoE（以太网协议封装的 PPP）的连接，正确的外部接口是 `tun0`，而不是物理以太网接口。

```sh
ext_if = "xl0"	# 外部接口的宏 - 对于 PPPoE 使用 tun0
int_if = "xl1"	# 内部接口的宏
localnet = $int_if:network
# ext_if 的 IP 地址可能是动态的，因此使用 ($ext_if)
nat on $ext_if from $localnet to any -> ($ext_if)
block all
pass from { lo0, $localnet } to any keep state
```

此规则集引入了 `nat` 规则，用于处理从内部网络中不可路由的地址到分配给外部接口的 IP 地址的网络地址转换。`nat` 规则中括号内的部分 `($ext_if)` 包括了外部接口的 IP 地址动态分配的情况。这确保了即使外部 IP 地址发生变化，网络流量也能正常运行。

请注意，此规则集可能允许比实际所需更多的流量通过。合理的设置可能会创建此宏：

```sh
client_out = "{ ftp-data, ftp, ssh, domain, pop3, auth, nntp, http, \
    https, cvspserver, 2628, 5999, 8000, 8080 }"
```

用于主通过规则：

```sh
pass inet proto tcp from $localnet to any port $client_out \
    flags S/SA keep state
```

可能还需要一些其他的通过规则。此规则启用外部接口上的 SSH：

```sh
pass in inet proto tcp to $ext_if port ssh
```

此宏定义和规则允许内部客户端使用 DNS 和 NTP：

```sh
udp_services = "{ domain, ntp }"
pass quick inet proto { tcp, udp } to any port $udp_services keep state
```

注意此规则中的 `quick` 关键字。由于规则集中包含多个规则，理解规则之间的关系非常重要。规则是按照它们在规则集中出现的顺序从上到下评估的。对于每个被 PF 评估的数据包或连接，*最后匹配的规则* 是应用的规则。然而，数据包与包含 `quick` 关键字的规则匹配时，规则处理会停止，数据包会按照该规则处理。需要对一般规则做出例外时，此特性非常有用。

### 34.3.2.2. 创建 FTP 代理

配置有效的 FTP 规则可能因 FTP 协议的特性而变得复杂。FTP 比防火墙早数十年出现，并且在设计上存在不安全因素。使用 FTP 的最常见问题包括：

* 密码以明文形式传输。
* 协议要求至少使用两个 TCP 连接（控制连接和数据连接），并且它们使用不同的端口。
* 会话建立后，数据通过随机选择的端口传输。

在考虑客户端或服务器软件中可能存在的安全漏洞之前，这些问题已构成安全挑战。更安全的文件传输替代方案包括 [sftp(1)](https://man.freebsd.org/cgi/man.cgi?query=sftp\&sektion=1\&format=html) 或 [scp(1)](https://man.freebsd.org/cgi/man.cgi?query=scp\&sektion=1\&format=html)，它们都具有认证功能并通过加密连接传输数据。

在需要使用 FTP 的情况下，PF 提供了将 FTP 流量重定向到名为 [ftp-proxy(8)](https://man.freebsd.org/cgi/man.cgi?query=ftp-proxy\&sektion=8\&format=html) 的小型代理程序，FreeBSD 系统的基本系统中包含了此程序。代理的作用是动态插入和删除规则集中的规则，通过一组锚点来正确处理 FTP 流量。

要启用 FTP 代理，请在 **/etc/rc.conf** 中添加以下行：

```sh
ftpproxy_enable="YES"
```

然后通过运行以下命令启动代理：

```sh
# service ftp-proxy start
```

对于基本配置，需要在 **/etc/pf.conf** 中添加三个元素。首先是代理用于插入为 FTP 会话生成的规则的锚点：

```sh
nat-anchor "ftp-proxy/*"
rdr-anchor "ftp-proxy/*"
```

其次，需要一条允许 FTP 流量进入代理的通过规则。

第三，需要在过滤规则之前定义重定向和 NAT 规则。将此 `rdr` 规则紧接在 `nat` 规则之后插入：

```sh
rdr pass on $int_if proto tcp from any to any port ftp -> 127.0.0.1 port 8021
```

最后，允许重定向的流量通过：

```sh
pass out proto tcp from $proxy to any port ftp
```

其中，`$proxy` 展开为代理守护进程绑定的地址。

保存 **/etc/pf.conf**，加载新规则，并从客户端验证 FTP 连接是否正常工作：

```sh
# pfctl -f /etc/pf.conf
```

此示例涉及基本设置，其中本地网络中的客户端需要联系其他地方的 FTP 服务器。此基本配置应该适用于大多数 FTP 客户端和服务器的组合。如 [ftp-proxy(8)](https://man.freebsd.org/cgi/man.cgi?query=ftp-proxy\&sektion=8\&format=html) 所示，代理的行为可通过向 `ftpproxy_flags=` 行添加选项以多种方式更改。一些客户端或服务器可能会有特定的差异，这些差异需要在配置中进行补偿，或者可能需要以特定的方式集成代理，例如将 FTP 流量分配到特定的队列中。

要通过 PF 和 [ftp-proxy(8)](https://man.freebsd.org/cgi/man.cgi?query=ftp-proxy\&sektion=8\&format=html) 运行受保护的 FTP 服务器，可在单独的端口上以反向模式配置独立的 `ftp-proxy`，使用 `-R` 选项，并配合其自己的重定向通过规则。

### 34.3.2.3. 管理 ICMP

许多用于调试或故障排除 TCP/IP 网络的工具依赖于互联网控制消息协议（ICMP），该协议的设计初衷就是用于调试。

ICMP 协议在主机和网关之间发送和接收 *控制消息*，主要是向发送方提供有关到达目标主机途中出现的异常或困难的反馈。路由器使用 ICMP 协商数据包大小和其他传输参数，此过程通常称为 *路径 MTU 发现*。

从防火墙的角度来看，一些 ICMP 控制消息易受已知攻击向量的威胁。此外，允许所有诊断流量不加限制地通过，虽然能简化调试，但也使其他人更容易提取网络信息。因此，以下规则可能不是最优的：

```sh
pass inet proto icmp from any to any
```

解决方案之一是允许来自本地网络的所有 ICMP 流量通过，同时阻止来自网络外部的探测请求：

```sh
pass inet proto icmp from $localnet to any keep state
pass inet proto icmp from any to $ext_if keep state
```

PF 还提供了其他选项，展示了其灵活性。例如，可指定用于 [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) 的消息类型，而非允许所有 ICMP 消息。首先，定义宏来表示这种消息类型：

```sh
icmp_types = "echoreq"
```

然后，使用此宏的规则：

```sh
pass inet proto icmp all icmp-type $icmp_types keep state
```

如果需要其他类型的 ICMP 包，可以将 `icmp_types` 扩展为一个包含这些包类型的列表。你可以输入 `more /usr/src/sbin/pfctl/pfctl_parser.c` 来查看 PF 支持的 ICMP 消息类型列表。也可以参考 [IANA 的 ICMP 参数文档](http://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml) 了解每种消息类型的含义。

Unix 的 `traceroute` 默认使用 UDP，因此还需要一个规则来允许 Unix `traceroute` 的使用：

```sh
# 允许 traceroute(8) 默认的端口范围：
pass out on $ext_if inet proto udp from any to any port 33433 >< 33626 keep state
```

由于 Microsoft Windows 系统上的 `TRACERT.EXE` 使用 ICMP 回显请求消息，因此仅需要第一条规则即可允许来自这些系统的网络追踪。Unix 的 `traceroute` 也可指示使用其他协议，使用 `-I` 参数时同样使用 ICMP 回显请求消息。更多细节可查看 [traceroute(8)](https://man.freebsd.org/cgi/man.cgi?query=traceroute\&sektion=8\&format=html) 的手册页。

#### 34.3.2.3.1. 路径 MTU 发现

互联网协议设计为设备独立的，设备独立性的后果之一是，给定连接的最佳数据包大小并不总是能可靠预测。数据包大小的主要限制是 *最大传输单元*（MTU），它设置了接口的最大数据包大小限制。输入 `ifconfig` 即可查看系统网络接口的 MTU 值。

TCP/IP 使用称为路径 MTU 发现的过程来确定连接的正确数据包大小。该过程会发送不同大小的数据包，并设置“禁止分片”标志，期望在达到上限时接收到类型为“3，代码 4”的 ICMP 返回包。类型 3 表示“目的地不可达”，代码 4 表示“需要分片，但已设置禁止分片标志”。要让路径 MTU 发现支持连接到其他 MTU，需将 `destination unreachable` 类型添加到 `icmp_types` 宏中：

```sh
icmp_types = "{ echoreq, unreach }"
```

由于 `pass` 规则已使用该宏，因此无需修改规则以支持新的 ICMP 类型：

```sh
pass inet proto icmp all icmp-type $icmp_types keep state
```

PF 能基于所有变种的 ICMP 类型和代码过滤。可能的类型和代码列表详见 [icmp(4)](https://man.freebsd.org/cgi/man.cgi?query=icmp\&sektion=4\&format=html) 和 [icmp6(4)](https://man.freebsd.org/cgi/man.cgi?query=icmp6\&sektion=4\&format=html)。

### 34.3.2.4. 使用表

有些类型的数据在某些时候对过滤和重定向很重要，但它们的定义太长，无法包含在规则集文件中。PF 支持使用表，表是可以在不重新加载整个规则集的情况下操作的定义列表，并且能提供快速查找。表名总是被 `< >` 括起来，如下所示：

```sh
table <clients> { 192.168.2.0/24, !192.168.2.5 }
```

在此示例中，**192.168.2.0/24** 网络是表的一部分，但地址 **192.168.2.5** 被排除在外，使用了 `!` 操作符。也可以从文件加载表，其中每个项目位于单独的行上，如此例 **/etc/clients**：

```sh
192.168.2.0/24
!192.168.2.5
```

要引用该文件，可以像这样定义表：

```sh
table <clients> persist file "/etc/clients"
```

定义了表后，就可以在规则中引用它：

```sh
pass inet proto tcp from <clients> to any port $client_out flags S/SA keep state
```

可以使用 `pfctl` 动态操作表的内容。以下示例将另一个网络添加到表中：

```sh
# pfctl -t clients -T add 192.168.1.0/16
```

请注意，通过这种方式所做的任何更改都会立即生效，非常适合测试，但断电或重启后不会保留。要使更改永久生效，请修改规则集中的表定义或编辑表所引用的文件。可以使用 [cron(8)](https://man.freebsd.org/cgi/man.cgi?query=cron\&sektion=8\&format=html) 作业定期将表的内容保存到磁盘，命令示例如 `pfctl -t clients -T show >/etc/clients`。或者，**/etc/clients** 可以通过以下命令更新为内存中的表内容：

```sh
# pfctl -t clients -T replace -f /etc/clients
```

### 34.3.2.5. 使用超载表保护 SSH

运行 SSH 的外部接口的用户可能在认证日志中看到如下内容：

```sh
Sep 26 03:12:34 skapet sshd[25771]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:34 skapet sshd[5279]: Failed password for root from 200.72.41.31 port 40992 ssh2
Sep 26 03:12:35 skapet sshd[5279]: Received disconnect from 200.72.41.31: 11: Bye Bye
Sep 26 03:12:44 skapet sshd[29635]: Invalid user admin from 200.72.41.31
Sep 26 03:12:44 skapet sshd[24703]: input_userauth_request: invalid user admin
Sep 26 03:12:44 skapet sshd[24703]: Failed password for invalid user admin from 200.72.41.31 port 41484 ssh2
```

这表明发生了暴力破解攻击，某些人或程序正在尝试发现正确的用户名和密码以进入系统。

如果需要允许合法用户通过外部 SSH 访问，更改 SSH 使用的默认端口可以提供一定的保护。然而，PF 提供了一种更优雅的解决方案。通过限制连接主机的操作，可将违反者驱逐到地址表中，拒绝其某些或所有访问权限。甚至可以丢弃超过限制的机器的所有现有连接。

要配置此功能，在规则集的表部分创建该表：

```sh
table <bruteforce> persist
```

然后，在规则集的较早位置添加规则，以阻止暴力破解访问并允许合法访问：

```sh
block quick from <bruteforce>
pass inet proto tcp from any to $localnet port $tcp_services \
    flags S/SA keep state \
    (max-src-conn 100, max-src-conn-rate 15/5, \
    overload <bruteforce> flush global)
```

括号中的部分定义了限制，数字应根据本地要求更改。可以按以下方式解读：

`max-src-conn` 是允许从单个主机发起的同时连接的数量。

`max-src-conn-rate` 是允许从任何单个主机发起的新连接速率（*15*）每多少秒（*5*）。

`overload <bruteforce>` 表示任何超出这些限制的主机，其地址将被添加到 `bruteforce` 表中。规则集会阻止来自 `bruteforce` 表中地址的所有流量。

最后，`flush global` 表示主机达到限制时，该主机的所有（`global`）连接都会被终止（`flush`）。

> **注意**
>
> 这些规则 *不会* 阻止慢速暴力破解者，如在 <http://home.nuug.no/~peter/hailmary2013/> 中所述。

此示例规则集主要作为说明。例如，如果希望允许大量连接，但希望在 SSH 方面更严格，可在规则集的较早位置使用类似以下的规则进行补充：

```sh
pass quick proto { tcp, udp } from any to any port ssh \
    flags S/SA keep state \
    (max-src-conn 15, max-src-conn-rate 5/3, \
    overload <bruteforce> flush global)
```

> **注意**
>
> **不一定需要阻止所有超载者：**
>
> 超载机制是通用技术，不仅适用于 SSH，并且并非总是需要完全阻止所有违反者。
>
> 例如，超载规则可以用于保护邮件服务或 Web 服务，并且可以在规则中使用超载表将违反者分配到一个队列中，给予最低带宽分配或重定向到特定的网页。

随时间推移，表将被超载规则填充，大小逐渐增大，占用更多内存。有时被阻止的 IP 地址是动态分配的地址，该地址已分配给有合法理由与本地网络中主机通信的主机。

对于这种情况，`pfctl` 提供了过期表项的功能。例如，以下命令将删除 `<bruteforce>` 表中在 `86400` 秒内未被引用的表项：

```sh
# pfctl -t bruteforce -T expire 86400
```

[security/expiretable](https://cgit.freebsd.org/ports/tree/security/expiretable/) 可提供类似功能，该工具删除在指定时间段内未被访问的表项。

安装后，可运行 expiretable 删除 `<bruteforce>` 表中超出指定时间的条目。此示例删除所有超过 24 小时的条目：

```sh
/usr/local/sbin/expiretable -v -d -t 24h bruteforce
```

### 34.3.2.6. 防止 SPAM

不要与内置的 spamassassin 配套使用的 spamd 守护进程混淆，[mail/spamd](https://cgit.freebsd.org/ports/tree/mail/spamd/) 可与 PF 配合使用，提供防御 SPAM 的外部防线。此 spamd 使用一组重定向与 PF 配置集成。

垃圾邮件发送者通常发送大量消息，垃圾邮件主要来自一些垃圾邮件友好的网络和大量被劫持的机器，这些机器通常会很快被报告到 *黑名单* 中。

从黑名单中的地址接收到 SMTP 连接时，spamd 会展示其横幅并立即切换到一种模式，逐字节地回应 SMTP 流量。这种技术旨在尽可能浪费垃圾邮件发送者的时间，称为 *tarpitting*。这种使用逐字节 SMTP 回复的具体实现通常称为 *stuttering*。

此示例演示了如何配置 spamd 与自动更新的黑名单。有关更多信息，请参考安装的 [mail/spamd](https://cgit.freebsd.org/ports/tree/mail/spamd/) 手册页。

**过程：配置 spamd**

1. 安装软件包或 Port [mail/spamd](https://cgit.freebsd.org/ports/tree/mail/spamd/)。要使用 spamd 的灰名单功能，必须将 [fdescfs(5)](https://man.freebsd.org/cgi/man.cgi?query=fdescfs\&sektion=4\&format=html) 挂载到 **/dev/fd**。将以下行添加到 **/etc/fstab**：

   ```sh
   fdescfs /dev/fd fdescfs rw 0 0
   ```

   然后，挂载文件系统：

   ```sh
   # mount fdescfs
   ```
2. 接下来，编辑 PF 规则集以包含：

   ```sh
   table <spamd> persist
   table <spamd-white> persist
   rdr pass on $ext_if inet proto tcp from <spamd> to \
       { $ext_if, $localnet } port smtp -> 127.0.0.1 port 8025
   rdr pass on $ext_if inet proto tcp from !<spamd-white> to \
       { $ext_if, $localnet } port smtp -> 127.0.0.1 port 8025
   ```

   `<spamd>` 和 `<spamd-white>` 两个表是必需的。来自 `<spamd>` 中列出的地址但不在 `<spamd-white>` 中的 SMTP 流量会被重定向到监听 8025 端口的 spamd 守护进程。
3. 接下来，在 **/usr/local/etc/spamd.conf** 中配置 spamd，并添加一些 **rc.conf** 参数。 安装 [mail/spamd](https://cgit.freebsd.org/ports/tree/mail/spamd/) 后，包含示例配置文件（**/usr/local/etc/spamd.conf.sample**）和 **spamd.conf** 的手册页。请参考这些文件，以获取更多配置选项。

   配置文件中，第一个不以 `#` 注释符号开头的行包含了定义 `all` 列表的块，该列表指定了要使用的黑名单：

   ```sh
   all:\
       :traplist:allowlist:
   ```

   该条目添加了所需的黑名单，多个黑名单之间用冒号（`:`）分隔。如果要使用白名单从黑名单中扣除地址，应将白名单的名称 *紧接* 在黑名单名称之后。例如：`:blocklist:allowlist:`。

   这后面是所指定的黑名单定义：

   ```sh
   traplist:\
       :black:\
       :msg="SPAM. Your address %A has sent spam within the last 24 hours":\
       :method=http:\
       :file=www.openbsd.org/spamd/traplist.gz
   ```

   第一行是黑名单的名称，第二行指定列表类型。`msg` 字段包含在 SMTP 对话中要显示给黑名单发送者的消息。`method` 字段指定 spamd-setup 获取列表数据的方法；支持的方式有 `http`、`ftp`、从挂载的文件系统中的 `file`，以及通过外部程序的 `exec`。最后，`file` 字段指定 spamd 期望接收的文件名。

   指定的白名单定义类似，但省略了 `msg` 字段，因为不需要消息：

   ```sh
   allowlist:\
       :white:\
       :method=file:\
       :file=/var/mail/allowlist.txt
   ```

> **技巧**
>
> **谨慎选择数据源：**
>
> 使用示例 **spamd.conf** 中的所有黑名单将会阻塞大量互联网地址。管理员需要编辑该文件，创建最佳配置，使用适当的数据源，并在必要时添加自定义列表。

接下来，将以下条目添加到 **/etc/rc.conf**。有关其他标志的说明，请参见手册页：

```sh
spamd_flags="-v" # 使用 "" 并查看 spamd-setup(8) 获取标志
```

完成后，重新加载规则集，输入 `service obspamd start` 启动 spamd，并使用 `spamd-setup` 完成配置。最后，创建 [cron(8)](https://man.freebsd.org/cgi/man.cgi?query=cron\&sektion=8\&format=html) 任务，定期调用 `spamd-setup` 更新表。

在典型的位于邮件服务器前的网关上，主机通常会在几秒钟到几分钟内开始拦截。

PF 还支持 *灰名单*，即临时拒绝来自未知主机的消息，使用 *45n* 错误码。来自灰名单主机的消息，如果在合理时间内再次尝试，将获准通过。遵循 RFC 1123 和 RFC 2821 限制的发送者的流量会立即通过。

有关灰名单技术的更多信息，请参见 [greylisting.org](http://www.greylisting.org/) 网站。灰名单最令人惊讶的地方，除了其简单性外，就是它仍然有效。垃圾邮件发送者和恶意软件编写者适应绕过此技术的速度非常慢。

**过程：配置灰名单**

1. 确保按照前述步骤 1 挂载了 [fdescfs(5)](https://man.freebsd.org/cgi/man.cgi?query=fdescfs\&sektion=4\&format=html)。
2. 要以灰名单模式运行 spamd，请在 **/etc/rc.conf** 中添加以下行：

   ```sh
   spamd_grey="YES"  # 如果是 YES，则使用 spamd 灰名单
   ```

   请参考 spamd 手册页，以了解其他相关参数的说明。
3. 完成灰名单设置：

   ```sh
   # service obspamd restart
   # service obspamlogd start
   ```

在后台，spamdb 数据库工具和 spamlogd 白名单更新程序为灰名单功能执行必要的操作。spamdb 是管理员基于 **/var/db/spamdb** 数据库内容管理黑名单、灰名单和白名单的主要界面。

### 34.3.2.7. 网络卫生

本节说明了如何使用 `block-policy`、`scrub` 和 `antispoof` 使规则集行为更加合理。

`block-policy` 可在规则集的 `options` 部分设置，该部分位于重定向和过滤规则之前。此选项决定规则阻止主机时，PF 是否会发送反馈。该选项有两个可能的值：`drop` 会丢弃被阻止的数据包，不发送反馈；`return` 会返回如 `Connection refused` 之类的状态码。

如果未设置，则默认策略为 `drop`。要更改 `block-policy`，指定所需的值：

```sh
set block-policy return
```

在 PF 中，`scrub` 是启用网络数据包规范化的关键字。该过程会重新组装分段的数据包，并丢弃具有无效标志组合的 TCP 数据包。启用 `scrub` 可提供一定的保护，防止某些基于不正确处理数据包碎片的攻击。虽然有多个选项可供选择，但最简单的形式适用于大多数配置：

```sh
scrub in all
```

一些服务，如 NFS，需要特定的碎片处理选项。有关更多信息，请参考 <https://home.nuug.no/~peter/pf/en/scrub.html>。

此示例会重新组装碎片，清除“do not fragment”标志，并将最大分段大小设置为 1440 字节：

```sh
scrub in all fragment reassemble no-df max-mss 1440
```

`antispoof` 机制通过阻止那些在接口和方向上逻辑上不可能出现的数据包，防范伪造或伪装 IP 地址的攻击。

这些规则会过滤来自外部世界的伪造流量，以及源自本地网络的任何伪造数据包：

```sh
antispoof for $ext_if
antispoof for $int_if
```

### 34.3.2.8. 处理不可路由地址

即使正确配置了网关来处理网络地址转换，有时也需要补偿他人的错误配置。常见的错误配置之一是允许来自不可路由地址的流量访问互联网。由于来自不可路由地址的流量可能会参与多种拒绝服务（DoS）攻击技术，因此建议明确阻止来自不可路由地址的流量进入网络的外部接口。

在此示例中，首先定义包含不可路由地址的宏，然后在阻止规则中使用它。流量往返这些地址会在网关的外部接口上静默丢弃。

```sh
martians = "{ 127.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, \
	      10.0.0.0/8, 169.254.0.0/16, 192.0.2.0/24, \
	      0.0.0.0/8, 240.0.0.0/4 }"

block drop in quick on $ext_if from $martians to any
block drop out quick on $ext_if from any to $martians
```

## 34.3.3. 启用 ALTQ

在 FreeBSD 上，ALTQ 可以与 PF 一起使用，以提供服务质量（QOS）。启用 ALTQ 后，就可以在规则集中定义队列，这些队列决定了出站数据包的处理优先级。

在启用 ALTQ 之前，请参阅 [altq(4)](https://man.freebsd.org/cgi/man.cgi?query=altq\&sektion=4\&format=html) 以确定系统上安装的网络卡驱动是否支持它。

ALTQ 不是一个可加载的内核模块。如果系统的接口支持 ALTQ，请按照 [配置 FreeBSD 内核](/di-10-zhang-pei-zhi-freebsd-nei-he/10.1.-gai-shu.md) 中的说明创建一个定制内核。以下是可用的内核选项。第一个选项是启用 ALTQ 所必需的。至少还需要一个选项来指定排队调度算法：

```sh
options         ALTQ
options         ALTQ_CBQ        # 基于类别的排队（CBQ）
options         ALTQ_RED        # 随机早期检测（RED）
options         ALTQ_RIO        # RED 输入/输出（RIO）
options         ALTQ_HFSC       # 层次化分组调度器（HFSC）
options         ALTQ_PRIQ       # 优先级排队（PRIQ）
```

以下是可用的调度算法：

**CBQ** 基于类别的排队（CBQ）用于将连接的带宽划分为不同的类别或队列，根据过滤规则优先处理流量。

**RED** 随机早期检测（RED）用于通过测量队列长度并将其与队列的最小值和最大值进行比较，避免网络拥塞。当队列长度超过最大值时，所有新数据包会被随机丢弃。

**RIO** 在随机早期检测输入/输出（RIO）模式下，RED 维护多个平均队列长度和多个阈值，每个 QOS 级别有一个阈值。

**HFSC** 层次化公平服务曲线分组调度器（HFSC），详见 <http://www-2.cs.cmu.edu/~hzhang/HFSC/main.html>。

**PRIQ** 优先级排队（PRIQ）始终优先通过较高队列中的流量。

有关调度算法和示例规则集的更多信息，可以参考 [OpenBSD 的网页档案](https://web.archive.org/web/20151109213426/http://www.openbsd.org/faq/pf/queueing.html)。
