我心目中最强的 FreeBSD 数据包过滤器 (pf.conf)
你好,我是无能。
这次我想介绍一下我最近折腾的封包过滤器(Packet Filter)。
当前设置
如下所示。
在 Packet Filter 的情况下:
NAT、rdr 等转换类规则会应用最先匹配到的规则。
pass、block 等过滤规则会应用 最后匹配到的规则。
这是其规格说明。
在这种情况下,为了提高可读性,我将所有过滤规则都设置为 quick,以便在匹配到规则时立即应用。
我会留下一些可以作为注释的内容。
set skip on lo
set block-policy drop
set optimization conservative
set state-policy if-bound
set ruleset-optimization basic
wanint="vtnet0"
# WireGuard Configuration
wg_ifs = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
#### -- Scrub Rules -- ####
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
#### -- NAT Rules -- ####
# WireGuard Clients NAT
nat on $wanint inet from <wg_clients> to any -> ($wanint)
block all
#### -- Fail2Ban Rules -- ####
anchor "f2b/*"
pass out quick keep state
#### -- UDP Rules -- ####
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
#### -- TCP Rules -- ####
# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state
set block-policy drop
所有不匹配规则的数据包都将被 drop(丢弃)。
如果是 return,对于 TCP,会返回 RST 通知对方并关闭连接。
对于 UDP,由于本身没有 TCP 连接,只会返回 ICMP port unreachable。
set optimization conservative
这是优化规则,在文档中被视为保守设置。
如果是 aggressive,会优先使连接过期,但在像 WireGuard 这样需要维持常连状态的环境中,仅因没有新数据包到来就会导致连接断开。
此外,还有 satellite 之类的选项,挺有浪漫感的。
high-latency A high-latency environment (such as a satellite connec- tion). satellite Alias for high-latency.
set state-policy if-bound
通过维持并记忆 state(状态)来通过数据包,从而防止不一致。
pfctl -ss 的结果就是 state,例如在以下配置中:
客户端 ←→ wg ←→ 服务器
由于 state 关联了“是在哪个接口上创建的”这一信息,通过仅允许通过相同接口的通信,可以防止不一致。
在普通 floating 的情况下:
入口:wg0
出口:vtnet0
返回:vtnet0 或 wg0 都可以
但是,在 state-policy if-bound 的情况下:
入口:wg0
出口:vtnet0
返回:必须是 wg0,否则不行
set ruleset-optimization basic
它会自动优化规则。
摘自官方文档:
remove duplicate rules(删除重复规则)
remove rules that are a subset of another rule(删除作为其他规则子集的规则)
combine multiple rules into a table when advantageous(在有利时将多个规则合并到一个表中)
re-order the rules to improve evaluation performance(重新排序规则以提高评估性能)
中文含义:
删除重复规则
删除作为其他规则子集的规则
在有利时将多个规则合并到一个表中
重新排序规则以提高评估性能
也就是说,即使是随手写的 pf 规则,它也能帮你整理得干干净净。只要开启这个功能,即使你按照自己觉得易读的方式编写 pf 规则,它也会自动帮你整理好,真是个神级功能。
变量定义
指定 WAN 接口名称
wanint="vtnet0"
WireGuard 上的变量定义和表格定义
wg_ifs = "{ wg0, wg1 }"
wg_net = "10.1.0.0/24"
wg_ports="{51820}"
table <wg_clients> const { 10.1.0.2, 10.1.0.4, 10.1.0.22 }
Scrub Rules
在这种情况下,为来自 WAN 的进入和出去的数据包都指定 MSS 值。
关于 all random-id,据说为了防止通过数据包识别出 OS 本身,但据我在 GNU/Linux 上使用 sudo tcpdump -n -v -i $interface 确认的情况来看,默认似乎已经启用了。
04:15:18.538048 IP (tos 0x0, ttl 52, id 61686, offset 0, flags [DF], proto TCP (6), length 52)04:15:18.538049 IP (tos 0x0, ttl 52, id 61687, offset 0, flags [DF], proto TCP (6), length 131)
04:15:18.538179 IP (tos 0x0, ttl 64, id 65430, offset 0, flags [DF], proto TCP (6), length 52)
04:15:18.538223 IP (tos 0x0, ttl 52, id 61688, offset 0, flags [DF], proto TCP (6), length 131)
此外,还设置了 in all 和 out all,以便对 WAN 接口以外的接口也进行 scrub。
scrub in on $wanint all random-id max-mss 1360
scrub out on $wanint all random-id max-mss 1360
scrub in all
scrub out all
NAT 规则
特定的 WireGuard 客户端通过 WireGuard 服务端 WAN 接口的 NAT 外出。
但是,在这种情况下只能通过 IPv4 外出。因为 WireGuard 服务端没有分配 IPv6 地址,所以只设置了 inet from。
nat on $wanint inet from <wg_clients> to any -> ($wanint)
Fail2Ban 规则
最后,这是使用 Fail2Ban 将攻击源 IP 添加到 pf 表并进行阻断的规则。通过 anchor "f2b/*" 将来自 pf 表中 IP 的数据包转发到 Fail2Ban 规则中。
由于需要比设置为 quick 的过滤规则更早匹配,因此将其放在上方。
由于紧接其前有默认规则 block all,因此在此处未匹配的数据包都将被 block。
/usr/local/etc/fail2ban/action.d/pf.conf:
# Option: block
#
# The action you want pf to take.
# Probably, you want "block quick", but adjust as needed.
# If you want to log all blocked use "blog log quick"
block = block quick
由于此设置会添加 block quick 的 Fail2Ban 规则,因此将其放在上位。
也就是说,首先通过 anchor "f2b/*" 将来自 pf 表中 IP 的数据包转发到 Fail2Ban 规则,然后:
anchor "f2b/*"
作为过滤规则,首先放置了允许传出数据包的规则。
pass out quick keep state
UDP 规则
WireGuard 服务端许可规则。
允许 51820/udp,并允许 $wg_ifs 上所有的 WireGuard 虚拟网卡。
为了防患于未然,这里也许应该设置得更严格一些?
pass in quick on $wg_ifs from $wg_net to any keep state
pass in quick on $wanint proto udp from any to ($wanint) port $wg_ports keep state
在这种情况下,由于已经维持了 state,所有通过 WireGuard 隧道的数据包都将被允许。如果顺序反了,由于 pass in quick on $wg_ifs from $wg_net to any keep state 规则是以 WireGuard 隧道为前提的,反过来则无法成立。
为了启用 HTTP/3,允许 443/udp。
# HTTP/3 Protocol
pass in quick on $wanint proto udp to ($wanint) port 443 keep state
TCP 规则
用过 nmap 等工具的人应该知道,TCP 端口扫描速度非常快,仅仅通过更改端口,攻击者很快就能发现。而对于 UDP 端口,nmap 扫描需要很长时间,因此为了更加安全,我设置了只能通过 WireGuard 隧道访问 SSH。也就是说,实际上采用将 TCP 封装在 UDP 中的配置应该是最安全的。
因为引入了 Fail2Ban,所以如果发生 SSH 攻击,会将攻击源 IP 添加到 pf 表并进行阻断。这实际上就像是一个蜜罐。
对于邮件协议,启用了 synproxy 以防止 SYN Flood 攻击。
在这种情况下,HTTP 使用 ($wanint) 指定目的 IP,是为了动态匹配分配给 WAN 接口的 IP 地址。没有明确指定 inet 是为了同时允许 IPv4 和 IPv6。
对于通过 IPv6 到达的数据包,由于使用 ($wanint) 指定了目的 IP,它将动态匹配分配给 WAN 接口的 IPv6 地址。
# Mail Protocols
pass in quick proto tcp from any to ($wanint) port {25, 465, 993, 995} synproxy state
# HTTP
pass in quick on $wanint proto tcp to ($wanint) port {80, 443} modulate state
以上,写得有点长了……