> 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/openbsd/networking-and-daemons/pf/nat.md).

# PF NAT

## 简介

网络地址转换（NAT）是将整个网络（或多个网络）映射到单个 IP 地址的方法。当互联网服务提供商分配给你的 IP 地址数少于需要接入互联网的计算机总数时，就需要 NAT。NAT 在 [RFC 1631](https://tools.ietf.org/html/rfc1631) 中描述。

NAT 让你能利用 [RFC 1918](http://tools.ietf.org/html/rfc1918) 描述的保留地址块。通常，内部网络会使用一个或多个这样的网络块来搭建。它们是：

```
10.0.0.0/8       (10.0.0.0 - 10.255.255.255)
172.16.0.0/12    (172.16.0.0 - 172.31.255.255)
192.168.0.0/16   (192.168.0.0 - 192.168.255.255)
```

做 NAT 的 OpenBSD 系统至少会有两块网络接口，一块接互联网，另一块接内部网络。NAT 会转换来自内部网络的请求，让它们看起来都来自 OpenBSD NAT 系统本身。

## NAT 工作原理

当内部网络上的客户端联系互联网上的机器时，会发出以该机器为目的地的 IP 数据包。这些数据包包含抵达目的地所需的全部寻址信息。NAT 关注这些信息：

* 源 IP 地址（如 **192.168.1.35**）
* 源 TCP 或 UDP 端口（如 2132）

数据包穿越 NAT 网关时会被修改，使其看起来像是来自 NAT 网关本身。NAT 网关会将所做的修改记录在状态表中，以便 a) 在返回数据包上反向还原这些修改，b) 确保返回数据包穿过防火墙而不被阻断。例如，可能做如下修改：

* 源 IP：替换为网关的外部地址（如 **198.51.100.1**）
* 源端口：替换为网关上随机选取的未用端口（如 53136）

内部机器与互联网主机都觉察不到这些转换步骤。对内部机器而言，NAT 系统只是互联网网关。对互联网主机而言，数据包看起来直接来自 NAT 系统；它完全不知道内部工作站的存在。

互联网主机回复内部机器的数据包时，会发往 NAT 网关的外部 IP（**198.51.100.1**）的转换端口（53136）。NAT 网关随后在状态表中查找，判断回复数据包是否匹配已建立的连接。会基于 IP/端口组合找到唯一匹配，告知 PF 这些数据包属于内部机器 **192.168.1.35** 发起的连接。PF 随后对返回数据包做与出站数据包相反的修改，并将其转发给内部机器。

ICMP 数据包的转换方式类似，但不修改源端口。

## IP 转发

由于 NAT 几乎总用在路由器与网络网关上，可能需要启用 IP 转发，让数据包能在 OpenBSD 机器的网络接口间穿行。IP 转发通过 sysctl 机制启用：

```sh
# sysctl net.inet.ip.forwarding=1
# echo  'net.inet.ip.forwarding=1' >> /etc/sysctl.conf
```

或针对 IPv6：

```sh
# sysctl net.inet6.ip6.forwarding=1
# echo  'net.inet6.ip6.forwarding=1' >> /etc/sysctl.conf
```

## 配置 NAT

NAT 以可选的 `nat-to` 参数形式指定给出站 `pass` 规则。通常不直接设在 `pass` 规则上，而是使用 `match` 规则。当数据包被 `match` 规则选中时，该规则中的参数（如 `nat-to`）会被记住，并在遇到匹配该数据包的 `pass` 规则时应用到数据包上。这允许一整类数据包由单条 `match` 规则处理，是否允许流量通过的具体决定则由 `block` 与 `pass` 规则做出。

`pf.conf` 中的通用格式大致如下：

```ini
match out on interface [af] \
   from src_addr to dst_addr \
   nat-to ext_addr [pool_type] [static-port]
[...]
pass out [log] on interface [af] [proto protocol] \
   from ext_addr [port src_port] \
   to dst_addr [port dst_port]
```

***match***

当数据包穿越规则集并匹配 `match` 规则时，该规则中指定的任何可选参数都会被记住以备后用（变为"粘性"）。

***pass***

此规则允许数据包被传输。若数据包先前被指定了参数的 `match` 规则匹配过，那些参数会应用到此数据包。`pass` 规则可有自己的参数；这些参数优先于 `match` 规则中指定的参数。

***out***

指定此规则适用的数据包流向。`nat-to` 仅可对出站数据包指定。

***log***

通过 pflogd 记录匹配的数据包。通常仅记录首个匹配的数据包。要记录所有匹配的数据包，使用 `log (all)`。

***interface***

传输数据包的网络接口名或组。

***af***

地址族，`inet` 表示 IPv4，`inet6` 表示 IPv6。PF 通常能根据源/目的地址推断此参数。

***protocol***

允许的数据包协议（如 tcp、udp、icmp）。若指定了 *src\_port* 或 *dst\_port*，则*必须*给出协议：

***src\_addr***

将被转换的数据包源（内部）地址。源地址可指定为：

* 单个 IPv4 或 IPv6 地址。
* [CIDR](https://web.archive.org/web/20150213012421/http://public.swbell.net/dedicated/cidr.html) 网络块。
* 完整限定域名，加载规则集时通过 DNS 解析。所有解析得到的 IP 地址都会替换进规则。
* 网络接口的名称或组。接口上的所有 IPv4 与 IPv6 地址会在加载时替换进规则。
* 网络接口名后跟 `/*netmask*`（如 `/24`）。接口上的每个 IP 地址与网络掩码组合成 CIDR 网络块，替换进规则。
* 网络接口名或组后跟以下任一修饰符：
  * `:network` — 替换为 CIDR 网络块（如 **192.168.0.0/24**）
  * `:broadcast` — 替换为网络广播地址（如 **192.168.0.255**）
  * `:peer` — 替换为点对点链路上对端的 IP 地址

    此外，`:0` 修饰符可附加在接口名/组或上述任一修饰符之后，表示 PF 不应将别名 IP 地址纳入替换。接口放在括号中时也可使用这些修饰符。例：`fxp0:network:0`
* [表](/openbsd/networking-and-daemons/pf/tables.md)。
* 上述任一项前加 `!`（"非"）修饰符取反。
* 使用[列表](/openbsd/networking-and-daemons/pf/lists-and-macros.md#lists)指定的一组地址。
* 关键字 `any`，表示所有地址

***src\_port***

第四层包头中的源端口。端口可指定为：

* 1 到 65535 之间的数字
* [services(5)](https://man.openbsdhandbook.com/services.5/) 中的有效服务名
* 使用[列表](/openbsd/networking-and-daemons/pf/lists-and-macros.md#lists)指定的一组端口
* 范围：
  * `!=`（不等于）
  * `<`（小于）
  * `>`（大于）
  * `<=`（小于或等于）
  * `>=`（大于或等于）
  * `><`（范围）
  * `<>`（反向范围）

    最后两个是二元运算符（接受两个参数），且不包含参数本身。
  * `:`（闭区间）

    闭区间运算符也是二元运算符，且包含参数本身。

`port` 选项在 `nat` 规则中通常不用，因为目的通常是无论端口如何都对所有流量做 NAT。

***dst\_addr***

将被转换的数据包目的地址。目的地址的指定方式与源地址相同。

***dst\_port***

第四层包头中的目的端口。此端口的指定方式与源端口相同。

***ext\_addr***

NAT 网关上数据包将被转换到的外部（转换）地址。外部地址可指定为：

* 单个 IPv4 或 IPv6 地址。
* [CIDR](https://web.archive.org/web/20150611113606/http://public.swbell.net/dedicated/cidr.html) 网络块。
* 完整限定域名，加载规则集时通过 DNS 解析。
* 外部网络接口的名称或组。接口上的所有 IP 地址会在加载时替换进规则。
* 外部网络接口名或组放在括号 `( )` 中。这告诉 PF：当命名接口上的 IP 地址变化时更新规则。这对通过 DHCP 或拨号获取 IP 地址的外部接口极为有用，无需每次地址变化都重载规则集。
* 网络接口名或组后跟以下任一修饰符：
  * `:network` — 替换为 CIDR 网络块（如 **192.168.0.0/24**）
  * `:peer` — 替换为点对点链路上对端的 IP 地址

    此外，`:0` 修饰符可附加在接口名/组或上述任一修饰符之后，表示 PF 不应将别名 IP 地址纳入替换。接口放在括号中时也可使用这些修饰符。例：`fxp0:network:0`
* 使用[列表](/openbsd/networking-and-daemons/pf/lists-and-macros.md#lists)指定的一组地址。

***pool\_type***

指定用于转换的[地址池](https://www.openbsdhandbook.com/pf/pools)类型。

***static-port***

告诉 PF 不要转换 TCP 与 UDP 数据包中的源端口。

这些行的最基本形式可能如下：

```pf
match out on tl0 from 192.168.1.0/24 to any nat-to 198.51.100.1
pass on tl0 from 192.168.1.0/24 to any
```

或者直接使用：

```pf
pass out on tl0 from 192.168.1.0/24 to any nat-to 198.51.100.1
```

此规则表示对来自 **192.168.1.0/24**、穿越 `tl0` 接口的所有数据包执行 NAT，将源 IP 地址替换为 **198.51.100.1**。

上面的规则虽然正确，但并非推荐形式。维护可能困难，因为外部或内部网络号任何变化都需要修改此行。与之对照，下面这行更易维护（`tl0` 为外部，`dc0` 为内部）：

```pf
pass out on tl0 inet from dc0:network to any nat-to tl0
```

优点相当明显：可任改两块接口的 IP 地址而无需修改此规则。注意，此情形下应指定 `inet` 以确保仅使用 IPv4 地址，避免意外。

如上以接口名指定转换地址时，IP 地址在 pf.conf *加载*时确定，而非动态确定。若用 DHCP 配置外部接口，这可能成为问题。若所分配的 IP 地址变化，NAT 会继续用旧 IP 地址转换出站数据包，导致出站连接停止工作。要规避此问题，可将接口名放在括号中，让 PF 自动更新转换地址：

```pf
pass out on tl0 inet from dc0:network to any nat-to (tl0)
```

此方法对 IPv4 与 IPv6 地址的转换都适用。

## 双向映射（1:1 映射）

使用 `binat-to` 参数可建立双向映射。`binat-to` 规则在内部 IP 地址与外部地址间建立一对一映射。这很有用，例如可为内部网络中的 Web 服务器提供自己的外部 IP 地址。从互联网到外部地址的连接会被转换到内部地址，而从 Web 服务器发出的连接（如 DNS 请求）会被转换到外部地址。`binat-to` 规则不会像 `nat` 规则那样修改 TCP 与 UDP 端口。

示例：

```conf
web_serv_int = "192.168.1.100"
web_serv_ext = "198.51.100.6"

pass on tl0 from $web_serv_int to any binat-to $web_serv_ext
```

## 转换规则例外

若需转换大多数流量，但在某些情况下需要例外，请确保例外由不含 `nat-to` 参数的过滤规则处理。例如，若上面的 NAT 示例修改为：

```
pass  out on tl0 from 192.168.1.0/24 to any nat-to 198.51.100.79
pass  out on tl0 from 192.168.1.208  to any
```

那么整个 **192.168.1.0/24** 网络的数据包都会被转换到外部地址 **198.51.100.79**，**192.168.1.208** 除外。

## 检查 NAT 状态

要查看活动的 NAT 转换，使用 pfctl 的 `-s state` 选项。此选项会列出当前所有 NAT 会话：

```sh
# pfctl -s state
fxp0 tcp 192.168.1.35:2132 (198.51.100.1:53136) -> 198.51.100.10:22 TIME_WAIT:TIME_WAIT
fxp0 udp 192.168.1.35:2491 (198.51.100.1:60527) -> 198.51.100.33:53   MULTIPLE:SINGLE
```

说明（仅第一行）：

*fxp0*

表示状态绑定的接口。若状态为[浮动](https://github.com/FreeBSD-Ask/openbsdhandbook/blob/main/networking-and-daemons/pf/options.html#state-policy)，则显示 `self`。

*tcp*

连接使用的协议。

*192.168.1.35:2132*

内部网络上机器的 IP 地址（**192.168.1.35**）。源端口（2132）显示在地址之后。这也是 IP 头部中被替换的地址。

*198.51.100.1:53136*

数据包被转换到的网关 IP 地址（**198.51.100.1**）与端口（53136）。

*198.51.100.10:22*

内部机器连接的目标 IP 地址（**198.51.100.10**）与端口（22）。

*TIME\_WAIT:TIME\_WAIT*

表示 PF 认为 TCP 连接所处的状态。


---

# 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/openbsd/networking-and-daemons/pf/nat.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.
