> 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.4-jie-packet-filter-pf.md).

# 29.4 Packet Filter（PF）

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

PF（Packet Filter，包过滤器）是一款源自 OpenBSD 的防火墙，具备队列管理等多种功能。

需要注意的是，FreeBSD 版本的 PF 与上游的 OpenBSD 版本多年来已存在显著差异。并非所有功能在 FreeBSD 上的表现都与 OpenBSD 中相同，反之亦然。

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

## 启用 PF

`pf` 模块随基本系统一同发布。启用 PF 前，需要先加载内核模块：

```sh
# kldload pf
```

> **注意**
>
> 若 PF 内核模块未加载，运行 `pfctl` 时会提示 `pfctl: /dev/pf: No such file or directory`。

默认情况下，PF 从 **/etc/pf.conf** 读取其配置规则，并根据该文件中指定的规则或定义修改、丢弃或放行数据包。复制示例文件作为默认配置规则集，否则 pf 无法启动：

```sh
# cp /usr/share/examples/pf/pf.conf /etc/
```

> **注意**
>
> 若未复制配置文件，将提示如下信息：
>
> ```sh
> /etc/rc.d/pf: WARNING: /etc/pf.conf is not readable.
> ```

还可在启动 PF 时传递其他选项，这些选项在 pfctl(8) 中有详细说明。将此条目添加至 **/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) 提供。要启用日志支持，执行命令：

```sh
# service pflog enable
```

还可添加以下行更改日志文件的默认位置，或指定启动 pflog(4) 时需要传递的附加标志：

```ini
pflog_enable="YES"              # 启用 pflogd 日志守护进程
pflog_logfile="/var/log/pflog"  # pflogd 应该存储日志文件的位置
pflog_flags=""                  # pflogd 启动时的附加标志
```

最后，若防火墙后面有局域网，且需要向局域网中的计算机转发数据包，或需要 NAT，需要在 **/etc/rc.conf** 中启用以下选项：

```ini
gateway_enable="YES" # 启用作为局域网网关
```

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

```sh
# service pf enable # 设置 pf 在系统启动时自动启动
# service pf start # 启动 pf 服务
# service pflog start # 启用日志支持
```

要控制 PF，可使用 `pfctl`。以下总结了该命令的一些常用选项，有关所有可用选项的说明，参阅 pfctl(8)：

**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** 中的错误，但不加载规则集。          |

为监控通过 PF 防火墙的流量，可考虑安装软件包或 Port **sysutils/pftop**。安装后，可运行 pftop 查看流量的实时快照，其格式类似于 top(1)。

## PF 规则集

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

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

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

第一条规则 `block in all` 默认拒绝所有传入的流量。

第二条规则 `pass out all keep state` 允许放行由该系统发起的连接，同时保留这些连接的状态信息。这些状态信息允许该连接的返回流量通过，且此功能应仅在受信任的计算机上使用。在 PF 中，`pass` 规则默认隐式创建状态，因此 `keep state` 可省略。

可通过以下命令加载该规则集：

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

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

```sh
# 允许出站访问的 TCP 服务
tcp_services = "{ ssh, submission, domain, www, https }"
# 允许出站访问的 UDP 服务
udp_services = "{ domain }"
```

PF 识别端口名以及端口号，只要在 **/etc/services** 中列出了这些名称。此例创建了两个宏，第一个是五个 TCP 端口名的列表（`ssh, submission, domain, www, https`），第二个是一个 UDP 端口名（`domain`）。定义后，即可在规则中使用宏。在此例中，除了该系统发起的五个指定的 TCP 服务连接和一个指定的 UDP 服务连接外，所有流量都会被阻止：

```sh
# 宏定义：允许出站访问的 TCP 与 UDP 服务
tcp_services = "{ ssh, submission, domain, www, https }"
udp_services = "{ domain }"
# 默认阻止所有流量
block all
# 允许出站的 TCP 连接，保持状态
pass out proto tcp to any port $tcp_services keep state
# 允许出站的 UDP 连接，保持状态
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` 选项，会显示完全解析后的规则，准确展示其加载方式。这在调试规则时非常有用。

### 简单的网关与 NAT

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

```sh
互联网
   │
   │  公网 IP
   ▼
em0（外部接口）
   │
   │  PF 防火墙 + NAT
   │  内网地址 → 外部接口 IP
   ▼
FreeBSD 网关
   │
   │  内网 IP（如 192.168.100.1/24）
   ▼
em1（内部接口）
   │
   ├── 内网主机 A
   ├── 内网主机 B
   └── 内网主机 C
```

首先，启用网关，使系统能将一个接口上接收到的网络流量转发至另一个接口。此 sysctl 设置将转发 IPv4 数据包：

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

要转发 IPv6 流量，使用：

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

要在系统引导时启用这些设置，使用 sysrc(8) 将它们添加到 **/etc/rc.conf** 中：

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

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

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

```sh
pass in on em1 from em1:network to em0:network port $ports keep state
```

该规则仅允许流量通过内部接口进入网关。若需要使数据包继续传递，还需要一条匹配的规则：

```sh
pass out on em0 from em1:network to em0:network port $ports keep state
```

虽然这两条规则有效，但此类特定规则通常并非必需。对繁忙的网络管理员而言，可读性高的规则集更安全。本节其余部分演示了如何使规则尽可能简单以便于阅读。例如，这两条规则可由一条规则替代：

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

`interface:network` 表示法可替换为宏，以使规则集更具可读性。例如，可定义一个 `$localnet` 宏，表示直接连接到内部接口的网络（`$em1: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 = "em0"    # 外部接口（PPPoE 拨号连接应使用 tun0）
int_if = "em1"    # 内部接口
localnet = $int_if:network
# 外部接口的 IP 地址可能是动态分配的，因此使用 ($ext_if) 使其随接口变化
block all
# 对来自内网的出站流量执行 NAT
match out on $ext_if from $localnet to any nat-to ($ext_if)
# 允许回环接口和内网主机发起的连接
pass from { lo0, $localnet } to any keep state
```

此规则集使用 `nat-to` 选项将内部网络中不可路由的地址转换为分配给外部接口的 IP 地址，即网络地址转换（NAT）。`nat-to` 选项中括号内的部分 `($ext_if)` 涵盖了外部接口的 IP 地址动态分配的情况，确保即使外部 IP 地址发生变化，网络流量也能正常运行。

需要注意的是，此规则集可能允许比实际所需更多的流量通过。更合理的做法是创建此宏：

```sh
# 客户端允许出站访问的 TCP 服务列表
client_out = "{ ssh, domain, http, \
    https, 8000, 8080 }"
```

用于主通过规则：

```sh
# 允许内网客户端访问常见的互联网 TCP 服务
pass inet proto tcp from $localnet to any port $client_out \
    flags S/SA keep state
```

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

```sh
# 允许外部接口接收 SSH 连接
pass in inet proto tcp to $ext_if port ssh
```

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

```sh
# 允许内部客户端使用的 UDP 服务（DNS、NTP）
udp_services = "{ domain, ntp }"
# quick 表示命中则立即通过，不再匹配后续规则
pass quick inet proto { tcp, udp } to any port $udp_services keep state
```

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

### 创建 FTP 代理

> **注意**
>
> FTP 协议已逐渐被 SFTP 和 SCP 等安全替代方案取代。现代网络中 FTP 的使用场景已大幅减少，以下 FTP 代理配置仅适用于仍需支持 FTP 的遗留环境。本节内容仍保留，因为 ftp-proxy 是演示 PF 锚点（anchor）机制的良好示例。

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

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

即使不考虑客户端或服务器软件中可能存在的安全漏洞，这些问题本身已构成安全挑战。更安全的文件传输替代方案包括 sftp(1) 或 scp(1)，它们均具备认证功能并通过加密连接传输数据。

在需要使用 FTP 的情况下，PF 可将 FTP 流量重定向至小型代理程序 ftp-proxy(8)，该程序包含于 FreeBSD 基本系统中。代理的作用是动态插入和删除规则集中的规则，通过一组锚点以正确处理 FTP 流量。

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

```sh
ftp_proxy_enable="YES"
```

同时，在 **/etc/pf.conf** 中定义代理监听地址的宏（ftp-proxy 默认监听 **127.0.0.1**）：

```sh
proxy = "127.0.0.1"
```

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

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

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

```sh
# FTP 代理规则的锚点：代理会在此处动态插入会话规则
anchor "ftp-proxy/*"
```

其次，需要一条将 FTP 流量重定向至代理的通过规则：

```sh
# 把来自内网的 FTP 控制流量重定向到本地 ftp-proxy（端口 8021）
pass in on $int_if proto tcp from any to any port 21 rdr-to 127.0.0.1 port 8021
```

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

```sh
# 允许 ftp-proxy 代理解析后的出站 FTP 流量通过
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) 所示，代理的行为可通过向 `ftp_proxy_flags=` 行添加选项以多种方式调整。一些客户端或服务器可能存在特定差异，需要在配置中做相应调整，或需要以特定方式集成代理，例如将 FTP 流量分配至特定队列。

若需要保护 FTP 服务器并配置 PF 与 ftp-proxy(8) 协同工作，可在独立端口上以反向模式运行 `ftp-proxy`，使用 `-R` 选项并配合专属的重定向通过规则。

### 管理 ICMP

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

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

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

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

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

```sh
# 允许内网发起的所有 ICMP 流量
pass inet proto icmp from $localnet to any keep state
# 允许外部到外部接口的 ICMP 流量（外部 ping 本机）
pass inet proto icmp from any to $ext_if keep state
```

PF 还提供了其他选项，展示了其灵活性。例如，可指定用于 ping(8) 和 traceroute(8) 的消息类型，而非允许所有 ICMP 消息。首先，定义一个宏表示此消息类型：

```sh
# 允许的 ICMP 消息类型：回显请求（ping）
icmp_types = "echoreq"
```

然后，使用该宏的规则：

```sh
# 仅允许指定类型的 ICMP 包通过
pass inet proto icmp all icmp-type $icmp_types keep state
```

若需要其他类型的 ICMP 包，可将 `icmp_types` 扩展为包含这些包类型的列表。可参阅 icmp(4) 和 icmp6(4) 手册页以了解 PF 支持的 ICMP 消息类型。也可参考 [IANA 的 ICMP 参数文档](https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml) 了解每种消息类型的解释。

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

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

Unix 的 `traceroute` 默认使用 UDP，也可指定使用其他协议，若使用 `-I` 参数，则使用 ICMP 回显请求消息。

#### 路径 MTU 发现（Path MTU Discovery）

互联网协议在设计上与设备无关，设备无关性导致的一个后果是，给定连接的最佳数据包大小并非总能可靠预测。数据包大小的主要限制是 **最大传输单元**（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
# 使用宏统一控制允许的 ICMP 类型；修改宏无需调整规则本身
pass inet proto icmp all icmp-type $icmp_types keep state
```

PF 允许基于所有变种的 ICMP 类型和代码过滤。可能的类型和代码列表在 icmp(4) 和 icmp6(4) 中有说明。

### 使用表

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

```sh
# 定义客户端表，允许 192.168.2.0/24 网络，但排除 192.168.2.5
table <clients> { 192.168.2.0/24, !192.168.2.5 }
```

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

```sh
# /etc/clients 示例：每个条目独占一行
192.168.2.0/24
!192.168.2.5
```

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

```sh
# 从文件持久加载表，规则集重新加载时不会清空表内容
table <clients> persist file "/etc/clients"
```

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

```sh
# 允许表中客户端访问常见出站 TCP 服务
pass inet proto tcp from <clients> to any port $client_out flags S/SA keep state
```

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

```sh
# 动态向 clients 表添加 192.168.1.0/16 网段
# pfctl -t clients -T add 192.168.1.0/16
```

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

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

### 使用超载表保护 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
# 首先阻止已在暴力破解表中的所有流量（quick 表示立即生效）
block quick from <bruteforce>
# 允许内网访问常见 TCP 服务，并设置连接速率限制；超限的源地址加入 <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` 是允许从任何单个主机每 *5* 秒发起的新连接速率（*15*）。

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

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

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

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

```sh
# 对 SSH 端口应用更严格的连接速率限制
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
```

### 网络卫生

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

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

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

```sh
# 被阻止时返回状态码（如 Connection refused），而非静默丢弃
set block-policy return
```

在 PF 中，`scrub` 是一个启用网络数据包规范化的关键字。该过程会丢弃具有无效标志组合的 TCP 数据包。启用 `scrub` 提供了一定的防护，可抵御某些基于不当处理数据包分片的攻击。

推荐将 `scrub` 作为 `match` 规则的选项使用，而非独立语句。最简单的形式适用于大多数配置：

```sh
# 对所有入站流量执行规范化：清除 DF 标志并随机化 IP ID
match in all scrub (no-df random-id)
```

对于分片重组，推荐在规则集的 `options` 部分使用 `set reassemble yes`：

```sh
# 启用分片重组，同时对置有 DF 标志的分片也进行重组（并清除标志）
set reassemble yes no-df
```

`no-df` 选项表示同时重组设置了“禁止分片”标志的分片，重组后的数据包将清除该标志。

NFS 等一些服务需要特定的分片处理选项。更多信息，参阅 [Firewalling with OpenBSD's PF packet filter](https://home.nuug.no/~peter/pf/en/scrub.html)。

以下示例通过 `match` 规则设置分片重组、清除 DF 标志，并将最大分段大小设置为 1440 字节：

```sh
# 启用分片重组、清除 DF 标志，并限制 TCP 最大分段大小为 1440
set reassemble yes no-df
match in all scrub (no-df max-mss 1440)
```

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

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

```sh
# 反地址欺骗：阻止在外部接口上逻辑上不可能出现的源/目的地址
antispoof for $ext_if
# 反地址欺骗：阻止在内部接口上逻辑上不可能出现的源/目的地址
antispoof for $int_if
```

### 处理不可路由地址

即使正确配置了网关处理网络地址转换，有时也需要弥补他人的错误配置。一种常见的错误配置是允许来自不可路由地址的流量访问互联网。由于来自不可路由地址的流量可能参与多种拒绝服务（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, 224.0.0.0/3 }"

# 在外部接口上阻止来自这些地址的入站流量
block drop in quick on $ext_if from $martians to any
# 在外部接口上阻止发往这些地址的出站流量
block drop out quick on $ext_if from any to $martians
```

## 其他 PF 功能

### SYN Cookie 防护

PF 提供了 `set syncookies` 选项，用于防御 SYN 洪水攻击。当 SYN cookie 激活时，PF 对每个入站 TCP SYN 回复 syncookie SYNACK，而不分配任何资源。收到客户端的 ACK 后，PF 评估规则集并在允许时创建状态。

```sh
# 从不发送 syncookie（默认行为）
set syncookies never
# 始终发送 syncookie（任何 SYN 都不分配状态）
set syncookies always
# 自适应模式：状态表占用 25% 时启用，回落到 12% 时关闭
set syncookies adaptive (start 25%, end 12%)
```

自适应模式在半开 TCP 连接占用状态表达到指定百分比时启用 syncookie 模式，在回落到较低百分比时关闭。

### 包速率限制

`max-pkt-rate` 选项可基于规则限制数据包速率。超过指定速率时，规则停止匹配：

```sh
# 限制 ICMP 入站速率为每 10 秒最多 100 个包，超出时该规则停止匹配
pass in proto icmp max-pkt-rate 100/10
```

此规则允许每 10 秒最多 100 个 ICMP 包通过，超速时该规则停止匹配。


---

# 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.4-jie-packet-filter-pf.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.
