FreeBSD 自带的 PF 防火墙已经足够强大。对个人服务器来说,一个比较稳的思路是:默认拒绝所有入站连接,只开放自己明确需要的端口,并让 PF 对高频连接来源自动加入黑名单。
这篇文章整理一套可落地的 PF 管理方式。核心目标有四个:
- 默认拒绝入站,允许出站。
- 自动保留当前正在监听的 SSH 端口,避免远程配置时把自己锁在门外。
- 按 TCP/UDP 分别开放端口,适合 SSH、Web、Hysteria2、sing-box 等不同服务。
- 对 TCP 高频连接启用
overload <blacklist>自动拉黑,并定期释放过期黑名单。
先启用 PF
用 root 用户执行:
sysrc pf_enable="YES"
sysrc pflog_enable="YES"如果是首次启用,先不要急着 service pf start。PF 服务启动时会读取 /etc/pf.conf,如果这个文件不存在或不可读,就会看到:
/etc/rc.d/pf: WARNING: /etc/pf.conf is not readable.所以首次启动前,先写入一个最小可用配置。下面这份配置只放行当前 SSH 端口和出站连接,避免远程操作时把自己锁在外面。
如果你的 SSH 不是 22 端口,把最后一行里的 22 换成你实际的 SSH 端口:
cat > /etc/pf.conf <<'EOF'
set skip on lo0
set block-policy drop
scrub in all fragment reassemble
block in all
pass out all keep state
pass in quick inet proto icmp all icmp-type echoreq keep state
pass in quick inet proto tcp from any to any port 22 keep state
EOF先检查语法:
pfctl -nf /etc/pf.conf没有报错后,再加载并启动:
kldload pf
service pf start已经启动过的系统,后续可以用:
service pf status检查 PF 状态。
如果你已经执行了 service pf start 并看到 /etc/pf.conf is not readable,不用慌。按上面的方式创建 /etc/pf.conf 后,再执行:
service pf start如果服务已经处于运行状态,则执行:
pfctl -f /etc/pf.conf最小 PF 配置思路
最小化规则大致是这样:
set skip on lo0
set block-policy drop
scrub in all fragment reassemble
block in all
pass out all keep state
pass in quick inet proto tcp from any to any port 22 keep state这表示:
- 跳过本机回环接口
lo0。 - 默认静默丢弃入站包。
- 允许所有出站连接并保留状态。
- 只开放 TCP 22 端口。
不过手动长期维护 /etc/pf.conf 容易漏端口,也容易误删 SSH 规则。下面这个脚本就是为了解决这件事。
推荐脚本
脚本已经整理到 GitHub 仓库:hanigege/scripts/pf_manager.sh。
在 FreeBSD 服务器上可以直接下载到 /root/pf-manager.sh:
fetch -o /root/pf-manager.sh https://raw.githubusercontent.com/hanigege/scripts/main/pf_manager.sh
chmod 700 /root/pf-manager.sh
/root/pf-manager.sh如果你更习惯用 curl,也可以这样执行:
curl -fsSL https://raw.githubusercontent.com/hanigege/scripts/main/pf_manager.sh -o /root/pf-manager.sh
chmod 700 /root/pf-manager.sh
/root/pf-manager.sh脚本设计说明
这个脚本的重点是稳妥维护 PF 规则,而不是把所有功能都堆进去。它主要做了几件事:
- 默认不提供“开放所有端口”选项,避免误操作后把服务器直接暴露到公网。
- 每次更新规则前,都会先执行
pfctl -nf做语法检查,通过后才覆盖/etc/pf.conf。 - TCP 和 UDP 分开处理:TCP 可以用连接数和连接频率触发自动拉黑,UDP 只限制单个来源的状态数量。
- 端口列表会自动规范化、排序、去重,避免重复写入同一个端口。
- 黑名单会同步保存到
/etc/pf_blacklist.txt,重启后仍然可以加载。 - 每日自动释放黑名单任务可以安装,也可以移除;任务已安装时,菜单会用红字显示
[已安装]。
TCP 与 UDP 的差异
PF 的自动拉黑常见写法是:
pass in quick inet proto tcp from any to any port 22 flags S/SA keep state (max-src-conn 80, max-src-conn-rate 30/10, overload <blacklist> flush global)这里适合 TCP,因为 TCP 有连接和握手状态。比如 SSH 端口遇到短时间大量连接,PF 能统计来源并触发 overload <blacklist>。
UDP 没有 TCP 那种连接握手,很多代理或游戏服务又天然会有高频 UDP 包。如果直接照搬 TCP 连接率限制,容易误判或者规则行为不符合预期。所以脚本里对 UDP 使用更保守的规则:
pass in quick inet proto udp from any to any port 443 keep state (max-src-states 80)这不会像 TCP 那样自动拉黑,但能限制单个来源制造过多状态。
开放端口示例
运行脚本后选择 3,输入端口和协议即可。
例如 SSH 是 TCP:
tcp 20789Hysteria2 常见是 UDP 443:
udp 443如果某个服务同时需要 TCP 和 UDP,再选择 TCP 和 UDP。
查看当前 PF 生效规则:
pfctl -sr查看状态表:
pfctl -ss查看黑名单:
pfctl -t blacklist -T show自动黑名单释放
永久封禁并不一定是好事。很多扫描来源是动态 IP,今天是攻击流量,过几天可能被运营商分给正常用户。黑名单长期膨胀也不利于审计。
比较平衡的做法是:每天释放 24 小时内没有继续活动的黑名单地址。
脚本菜单里选择 10 会安装下面这条 crontab:
0 3 * * * /sbin/pfctl -t blacklist -T expire 86400 >/dev/null 2>&1 && /sbin/pfctl -t blacklist -T show > /etc/pf_blacklist.txt安装成功后,脚本会把写入的任务打印出来。再次回到菜单时,第 10 项后面会显示红色的 [已安装]:
10. 安装每日自动释放黑名单任务 [已安装]
11. 移除每日自动释放黑名单任务你也可以随时运行下面的命令确认:
crontab -l | grep 'pfctl -t blacklist'如果能看到上面的 0 3 * * * ... 这一行,就表示已经安装成功。
如果以后不想自动释放黑名单了,在脚本菜单里选择 11,确认后会移除这条 crontab。移除后再执行上面的 grep 命令,正常情况下不会再看到这条任务。
含义是:
- 每天凌晨 3 点执行。
expire 86400会释放 86400 秒内没有活动的表项。- 清理后把内核表重新写回
/etc/pf_blacklist.txt。
也可以手动执行一次:
pfctl -t blacklist -T expire 86400
pfctl -t blacklist -T show > /etc/pf_blacklist.txt测试自动拉黑
可以从另一台机器,对服务器上“已有服务正在监听,并且 PF 规则允许外部访问”的 TCP 端口做并发连接测试。这里的监听者是 SSH、Web 服务或其他业务程序,不是 PF 防火墙本身;PF 只负责放行、丢弃和统计连接状态,不会替业务程序监听端口。
测试机器上需要有 nc 命令。很多精简系统默认没有安装,可以按系统选择安装:
# Debian / Ubuntu
apt install -y netcat-openbsd
# RHEL / Rocky / AlmaLinux / Fedora
dnf install -y nmap-ncat
# Alpine Linux
apk add --no-cache netcat-openbsd正式并发测试前,先做一次单连接确认:
nc -z -v -w 3 你的服务器IP 20789如果这里显示 succeeded 或类似“连接成功”的提示,才说明这个端口确实能从测试机器连到服务器。然后再做并发测试:
for i in $(seq 1 100); do
nc -z -v -w 1 你的服务器IP 20789 &
done
wait然后在服务器上查看:
pfctl -t blacklist -T show需要注意:如果目标 TCP 端口虽然被 PF 规则放行,但服务器上没有程序监听,客户端通常会看到 Connection refused。这时连接会被系统快速重置,PF 状态不会堆积,可能不会触发自动拉黑。这不代表 PF 没生效,而是端口上没有真实服务承接连接。
如果目标端口没有被 PF 放行,通常会表现为超时,因为 PF 在最外层直接丢弃了包,连接根本到不了服务器上的业务程序。
如果并发测试里大量看到 timed out,但黑名单仍然是空的,通常说明这些连接没有命中带 overload <blacklist> 的放行规则。先在服务器上确认这个端口确实处于监听状态,并且 PF 规则里有对应的 TCP 放行规则:
sockstat -4 -l -P tcp
pfctl -sr远程管理用的 SSH 端口不建议反复拿来做压力测试。更稳妥的方式是临时开放一个测试 TCP 端口,确认有测试服务监听后,再从另一台机器并发连接这个测试端口。
防止远程锁死自己
远程配置防火墙时,建议按这个顺序做:
- 保留一个已经登录的 SSH 终端,不要关闭。
- 运行脚本,先确认它自动识别到了当前 SSH 端口。
- 新开一个 SSH 窗口测试能否登录。
- 再关闭旧窗口。
如果规则写错,旧窗口还在,就可以马上修复:
pfctl -d或者重新运行脚本生成规则。
总结
这套 PF 配置适合个人 FreeBSD 服务器长期使用:默认拒绝入站,明确开放端口,TCP 高频连接自动拉黑,黑名单定时释放。
最推荐的日常操作是:
/root/pf-manager.sh然后只通过菜单维护开放端口和黑名单,尽量不要手动来回改 /etc/pf.conf。