用 FreeBSD PF 构建可检查的双网卡 NAT 网关

把一台 FreeBSD 机器放在内网与外网之间,需要同时处理三件事:允许内核转发数据包,为私有地址配置 NAT,再用防火墙规则决定哪些连接可以经过。只写 NAT 并不等于已经允许流量;只允许数据包进入内侧网卡,也不等于它能从外侧网卡继续发出。

本文根据 FreeBSD Handbook 第 34 章的防火墙基础、PF 启用、规则集及 “A Simple Gateway with NAT” 小节完整整理这条操作路线。源页其余 IPFW、IPFILTER、FTP 代理、邮件过滤、ALTQ 和 Blocklistd 内容不属于本篇网关任务。FreeBSD 的 PF 已与 OpenBSD 版本产生明显差异,不能直接把当前 OpenBSD 语法当作 FreeBSD 上的保证。

来源是 FreeBSD Documentation Project。核对时手册首页列出 FreeBSD 15.1-RELEASE、14.4-RELEASE 和 13.5-RELEASE,并提醒部分章节可能过时。本文保留原例接口名和语法,需以实际系统的 pf.conf(5)、pfctl(8) 及解析结果为准。本次仅做静态审查,没有启用防火墙、修改转发设置、开放端口或测试联网。

双网卡 FreeBSD PF 网关位于私有内网和外网之间,内网经 xl1 进入、由 xl0 发出,转发、状态过滤和 NAT 各负其责
基础双网卡网关的职责与数据路径。未完纪绘制,接口名来自官方示例;不是运行拓扑截图。

先分清状态过滤与地址转换

防火墙根据协议、源地址、目标地址以及端口等字段决定放行或阻断。可以采用“默认放行,列出例外阻断”,也可以采用“默认阻断,只允许明确列出的流量”。本篇沿用后一种思路。

状态过滤将新连接作为主要决策对象。连接获准后,PF 会记录状态,后续符合该状态的双向数据包可以通过。UDP 虽然没有 TCP 式连接,PF 仍能记录一定的请求与响应状态,例如允许 DNS 查询对应的返回包。这不代表状态表能够证明应用层内容安全。

NAT 则改写地址,使内网设备能够共享外侧地址,并对返回流量做相应反向转换。原文列出的 IPv4 私有地址范围是 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16。这些地址不能被直接当作公网可路由地址使用;地址转换与过滤规则仍然是两个需要分别核对的环节。

准备两块接口和可以恢复的管理入口

原例把 xl0 连接到外网,把 xl1 连接到内网。它们是示例中的 3Com 接口名,不应照抄到使用其他驱动的机器。先用 ifconfig 查看真实接口、地址和链路状态,确认两块网卡接入的是不同网络。拨号或 PPPoE 场景中,外侧可能是 tun0,原文特别提醒不要把底层物理以太网接口误当作 PPPoE 的实际出口。

ifconfig

内网客户端应位于内侧接口能到达的子网,并以这台机器作为所需的默认网关;网关本身也应有正确的外侧路由。这里补充的是完整链路条件:防火墙规则不能替代 IP 地址与路由配置。

首次配置应从本地控制台或可靠的带外控制台操作,并保留经过核对的旧配置与恢复方法。远程 SSH 连接仍然存在,不代表新建管理连接一定能通过新规则;旧连接可能暂时依赖已有状态继续工作。本文没有把恢复操作写成自动执行脚本。

先准备规则文件,再启用 PF

FreeBSD 基础系统包含 PF 支持,但默认不附带适合当前网络的规则集,通常也没有现成的 /etc/pf.conf。示例文件可在 /usr/share/examples/pf/ 查找。PF 找不到规则文件时不会按预期启动,因此应先创建并核对规则,再进入启用步骤。

原文用以下持久配置开启 PF 与日志服务。这些命令会改写 /etc/rc.conf,需要相应管理权限;下列内容是教程说明,本次未执行:

sysrc pf_enable=yes
sysrc pflog_enable=yes

默认规则路径是 /etc/pf.conf。若采用其他路径,可在 /etc/rc.conf 设置 pf_rules="/path/to/pf.conf",并将其替换成真实的绝对路径。pf_flags="" 用于附加启动参数。日志由 pflog 支持,原文给出的默认日志配置为:

pflog_logfile="/var/log/pflog"
pflog_flags=""

编辑澄清:启动日志服务不会自动让所有规则产生日志。要记录特定流量,还需要在对应 PF 规则中使用 log。日志可能包含网络地址等敏感信息,并占用磁盘空间,应按实际需要配置和保留。

理解最简单的规则,再把它扩展到网关

对于一台不提供入站服务、仅主动访问网络的可信单机,原文首先给出:

block in all
pass out all keep state

第一行阻止新的入站流量,第二行允许本机向外发起连接,并保留状态让回应回来。这只是理解状态过滤的起点,不是双网卡 NAT 网关的完整配置,更不是所有机器都适用的安全默认值。

PF 支持列表与宏。宏必须在使用前定义,端口可以写数字,也可以写 /etc/services 中的服务名。原文演示过按 TCP、UDP 分别建立服务列表;其中包含 SMTP、POP3 等历史示例服务,不应因它们出现在教程中就开放。面向实际网络时,应先列出业务需要,再建立对应的协议与端口规则。

“进入网关”与“穿过网关”是不同事件

原文用下面两条规则说明方向。$ports 在这里只代表一个已经定义的允许端口宏,单独复制这几行并不是完整配置:

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

第一条允许从内侧进入网关,第二条对应从外侧离开。to 指定的是目标地址条件,不能被理解为“自动穿过所有接口”。原文接着将演示简化为不限定方向的规则:

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

interface:network 代表接口关联的网络。把它写成 localnet 宏,可以减少重复并提高可读性。若内网包含多个网段,也可以根据实际拓扑用网络列表定义。接口、地址范围、协议和端口都应在同一张拓扑表中核对,不能仅检查语法。

基础 NAT 网关规则集

下面完整保留官方基础规则集,并将注释译成中文。它用于说明内网主动访问外网的基本路径:

ext_if = "xl0"    # 外侧接口;PPPoE 示例可能使用 tun0
int_if = "xl1"    # 内侧接口
localnet = $int_if:network

# 外侧地址可能动态变化,因此使用括号形式
nat on $ext_if from $localnet to any -> ($ext_if)

block all
pass from { lo0, $localnet } to any keep state

nat 把内网源地址转换成外侧接口的地址。($ext_if) 中的括号使规则能够跟随接口地址变化,而不是仅保留加载规则时解析出的地址。它解决动态地址引用问题,但不能保证公网地址变化后原有远程 TCP 会话仍然无缝延续。

block all 建立阻断基线,最后的 pass 放行来自回环和内网地址的有状态流量。NAT 在前、过滤规则在后,是这一 FreeBSD PF 示例的规则组织方式。

静态审查发现:官方也承认,这个规则集放行的出站流量通常比实际需要更多。它没有细分业务目的地和端口,而且基于源地址的放行并不等于已经实施完整的入口反欺骗策略。它适合作为理解 NAT 与状态机制的教学基线,不应被描述为已经加固的生产配置。

启用转发,并区分 IPv4 与 IPv6

允许内核在接口间转发 IPv4 数据包的即时设置是:

sysctl net.inet.ip.forwarding=1

要在系统启动时保留这一设置,原文使用:

sysrc gateway_enable=yes

IPv6 是另一组独立选项:

sysctl net.inet6.ip6.forwarding=1
sysrc ipv6_gateway_enable=yes

这些命令会改变机器作为路由器的行为。仅在确实设计了相应转发和过滤策略时启用。本文的私有地址 NAT 主线面向 IPv4;不能因为配置了 IPv4 NAT,就认为 IPv6 的地址、路由和访问控制也已经处理完毕。没有 IPv6 转发需求时,无需照抄 IPv6 启用命令。

先只解析,再加载和观察

每次修改规则后,先让 pfctl 解析而不加载:

pfctl -vnf /etc/pf.conf

-n 表示只解释规则,-v 会显示展开后的内容,方便检查宏、地址和顺序。语法解析能发现语法层面的错误,不能证明接口选择正确,也不能证明某个连接会按预期获准。本文没有运行这一解析命令,因此不声称这里的配置已经在任何具体 FreeBSD 版本上通过检查。

在规则文件与管理入口已准备妥当后,原文的服务启动命令是:

service pf start
service pflog start

PF 已经运行时,用以下命令加载修改过的规则:

pfctl -f /etc/pf.conf

原文说明,加载没有语法错误时通常不会打印消息。没有输出不能替代成功状态与后续行为检查。最近一次有效加载的规则会持续生效,直到规则被替换或 PF 被禁用。

观察时将过滤规则、NAT 规则和状态表分开查看:

pfctl -s rules
pfctl -s nat
pfctl -s states

先确认宏已经展开到正确接口与地址,再从隔离内网客户端发起一条预期允许的连接,观察相应状态与转换;随后检查一条预期阻断的连接,并确认新建管理连接可用。这里描述的是读者应执行的验证过程,没有捏造状态表或连通性结果。若只检查已有连接,可能错过新规则对新会话的影响。

原文的命令表还列出了禁用 PF 以及清空全部 NAT、过滤、状态和表后重新加载的选项。它们会实质影响保护与现有连接,尤其 pfctl -F all -f /etc/pf.conf 不是普通的“刷新查看”命令。本文保留其风险说明,不将清空状态作为日常检查步骤,也没有执行它。

若新规则不符合预期,应从保留的控制台恢复已经审核的旧规则文件,并重新核对规则及状态。恢复旧规则和清空现有状态是两种不同动作,不能不加区分地全部执行;具体恢复流程应在投入实际网络之前演练。

收紧服务范围时,注意规则顺序

原文随后介绍按服务端口收紧出站流量,并演示在外侧提供 SSH。原例中外侧 SSH 规则没有限制来源,DNS/NTP 的 quick 规则也没有限制源网络;若直接复制,它们的授权范围比“只让内网客户端使用这些服务”更宽。

下面给出一个编辑收紧片段,用于说明如何在原 NAT 定义和阻断基线之后,按 IPv4、协议、内网源地址及端口限制新连接。它取代原先允许内网所有流量的宽泛规则;不能把它追加到那条宽规则后面,然后误以为宽泛授权已经消失:

# 编辑示例:以下是过滤片段,不是独立完整配置
pass from lo0 to any keep state

# 仅示例性允许内网 HTTP/HTTPS,以及 DNS 和 UDP NTP
pass inet proto tcp from $localnet to any port { 80, 443 } flags S/SA keep state
pass inet proto { tcp, udp } from $localnet to any port 53 keep state
pass inet proto udp from $localnet to any port 123 keep state

与原文相比,这里移除了历史服务列表中的 FTP、POP3 等非必要演示端口,把 DNS 限于 53、NTP 限于 UDP 123,并明确了内网源网络和 IPv4。该片段仍未把 DNS/NTP 目标限制到指定服务器,也没有涵盖网关自身更新、DHCP、ICMP 路径 MTU、管理流量或完整反欺骗策略;它只是收紧方法示例,尚未运行或验证,不能当作通用成品规则集。

如需 SSH 管理,应按实际管理来源、目标接口和地址明确授权;不应原样采用来自任意地址的公网 SSH 示例。这里不猜测管理员的地址,也不加入一个可能锁死访问或暴露服务的虚构生产规则。

PF 过滤规则通常按从上到下的顺序求值,并由最后一条匹配规则决定动作;匹配带有 quick 的规则时,会立即停止后续规则处理。已有状态还会参与数据包处理。因此,一条早期的宽泛 pass quick 会使后面的限制失去机会,不能只看规则文字是否“包含 block”就判断有效。

把“可以解析”与“达到网络目标”分开验收

一份可检查的网关配置应能回答:内外接口是否与真实连线一致,客户端与网关路由是否正确,IPv4/IPv6 转发是否按需开启,NAT 是否作用于正确的源网段,过滤规则是否只允许计划中的新连接,以及管理与恢复路径是否可用。规则表、NAT 表和状态表分别提供不同证据。

本次静态审核没有在展示片段中发现硬编码凭据或把外部输入拼接到命令中的写法,但确实发现并标注了宽泛出站授权、未限定来源的服务示例以及清空状态造成中断的风险。没有运行任何系统命令或联网验证,也不能据静态阅读断言配置不存在漏洞。将示例移到实际机器之前,仍需在隔离网络中检查版本语法、业务允许与阻断路径及回退行为。

来源与归属:FreeBSD Handbook,Chapter 34. Firewalls,The FreeBSD Documentation Project。本文按基础 PF NAT 网关主题翻译整理,编辑收紧片段与补充已明确标注。手册版权:Copyright © 1995–2026 The FreeBSD Documentation Project。以下保留官方文档许可条款。

The FreeBSD Documentation License
Copyright 1994-2026 The FreeBSD Project.

Redistribution and use in source (AsciiDoc) and 'compiled' forms (HTML, PDF,
EPUB and so forth) with or without modification, are permitted provided that
the following conditions are met:

1. Redistributions of source code (AsciiDoc) must retain the above copyright
notice, this list of conditions and the following disclaimer as the first
lines of this file unmodified.
2. Redistributions in compiled form (Converted to PDF, EPUB and other
formats) must reproduce the above copyright notice, this list of conditions
and the following disclaimer in the documentation and/or other materials
provided with the distribution.

THIS DOCUMENTATION IS PROVIDED BY THE FREEBSD DOCUMENTATION PROJECT "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED.
IN NO EVENT SHALL THE FREEBSD DOCUMENTATION PROJECT BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

许可来源:The FreeBSD Documentation License。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容