我心目中最強的 FreeBSD 封包過濾器 (pf.conf)

8 min

language: ja bn en es hi pt ru zh-cn zh-tw

你好,我是無能。

這次我想介紹一下我最近在折騰的封包篩選器(Packet Filter)。

目前的設定

目前是這樣的。
在 Packet Filter 的情況下,
NAT、rdr 等轉換系的規則會套用最先匹配(Match)到的規則,
而 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,否則為 NG

set ruleset-optimization basic

它會自動幫你優化規則。
節錄自官方文件:

  1. 移除重複的規則

  2. 移除屬於其他規則子集的規則

  3. 在有利時將多個規則合併到表格中

  4. 重新排序規則以提高評估效能

中文翻譯如下:

  1. 重複しているルールを削除する(刪除重複的規則)

  2. 他のルールに包含されているルールを削除する(刪除被其他規則包含的規則)

  3. 複数のルールを、必要に応じてテーブルにまとめる(視需要將多個規則合併到表格中)

  4. 評価性能を向上させるためにルールの順序を並べ替える(為了提高評估效能而重新排列規則順序)

也就是說,即使是隨便寫寫的 pf 規則,它也能幫你整理得很乾淨。只要啟用這個功能,就算你按照自己的喜好寫出易讀的 pf 規則,它也會自動幫你整理好,真是個神功能。

變數定義

指定 WAN 介面名稱

wanint="vtnet0"

定義為分配到的 IPv4 位址,且作為明確的出口 IP

exsrv1 = "163.44.113.145"

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 allout 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 Rules

特定的 WireGuard 用戶端會透過 WireGuard 伺服器端的 WAN 介面 NAT 出去。
但在此情況下,只能透過 IPv4 出去。因為 WireGuard 伺服器端沒有分配 IPv6 位址,所以只設定了 inet from

nat on $wanint inet from <wg_clients> to any -> ($wanint)

Fail2Ban Rules

這是用於讓 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 Rules

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 Rules

有用過 nmap 等工具的人應該都知道,TCP 的連接埠掃描速度非常快,光是更改連接埠,攻擊者很快就能發現。由於 nmap 掃描 UDP 連接埠需要花費很多時間,為了更安全,我設定成只能透過 WireGuard 隧道存取 SSH。也就是說,實質上採用將 TCP 封裝在 UDP 中的架構,我認為是最安全的配置。
因為導入了 Fail2ban,如果發生 SSH 攻擊,會將攻擊來源 IP 加入 pf 表格並封鎖。這感覺就像是實質上的誘餌系統(Honeypot)。

關於郵件協定,啟用了 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

以上,寫得有點長了……。

Related Posts